mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-09 19:50:18 +00:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aaa65e33dc |
@@ -91,8 +91,6 @@ TINYAUTH_APPS_name_LDAP_GROUPS=
|
|||||||
|
|
||||||
# Comma-separated list of allowed OAuth domains.
|
# Comma-separated list of allowed OAuth domains.
|
||||||
TINYAUTH_OAUTH_WHITELIST=
|
TINYAUTH_OAUTH_WHITELIST=
|
||||||
# Path to the OAuth whitelist file.
|
|
||||||
TINYAUTH_OAUTH_WHITELISTFILE=
|
|
||||||
# The OAuth provider to use for automatic redirection.
|
# The OAuth provider to use for automatic redirection.
|
||||||
TINYAUTH_OAUTH_AUTOREDIRECT=
|
TINYAUTH_OAUTH_AUTOREDIRECT=
|
||||||
# OAuth client ID.
|
# OAuth client ID.
|
||||||
|
|||||||
@@ -49,11 +49,6 @@ jobs:
|
|||||||
run: |
|
run: |
|
||||||
cp -r frontend/dist internal/assets/dist
|
cp -r frontend/dist internal/assets/dist
|
||||||
|
|
||||||
- name: Lint backend
|
|
||||||
uses: golangci/golangci-lint-action@v9
|
|
||||||
with:
|
|
||||||
version: v2.12
|
|
||||||
|
|
||||||
- name: Run tests
|
- name: Run tests
|
||||||
run: go test -coverprofile=coverage.txt -v ./...
|
run: go test -coverprofile=coverage.txt -v ./...
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +0,0 @@
|
|||||||
version: "2"
|
|
||||||
linters:
|
|
||||||
settings:
|
|
||||||
errcheck:
|
|
||||||
exclude-functions:
|
|
||||||
- (http.ResponseWriter).Write
|
|
||||||
- (http.ResponseWriter).WriteString
|
|
||||||
- (github.com/gin-gonic/gin.ResponseWriter).Write
|
|
||||||
- (github.com/gin-gonic/gin.ResponseWriter).WriteString
|
|
||||||
exclusions:
|
|
||||||
rules:
|
|
||||||
- linters:
|
|
||||||
- errcheck
|
|
||||||
text: "//nolint:errcheck"
|
|
||||||
- linters:
|
|
||||||
- staticcheck
|
|
||||||
text: "//nolint:staticcheck"
|
|
||||||
@@ -65,7 +65,7 @@ Tinyauth is licensed under the GNU General Public License v3.0. TL;DR — You ma
|
|||||||
|
|
||||||
A big thank you to the following people for providing me with more coffee:
|
A big thank you to the following people for providing me with more coffee:
|
||||||
|
|
||||||
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/ax-mad"><img src="https://github.com/ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a> <a href="https://github.com/stegratech"><img src="https://github.com/stegratech.png" width="64px" alt="User avatar: stegratech" /></a> <a href="https://github.com/apearson"><img src="https://github.com/apearson.png" width="64px" alt="User avatar: apearson" /></a> <!-- sponsors -->
|
<!-- sponsors --><a href="https://github.com/erwinkramer"><img src="https://github.com/erwinkramer.png" width="64px" alt="User avatar: erwinkramer" /></a> <a href="https://github.com/nicotsx"><img src="https://github.com/nicotsx.png" width="64px" alt="User avatar: nicotsx" /></a> <a href="https://github.com/SimpleHomelab"><img src="https://github.com/SimpleHomelab.png" width="64px" alt="User avatar: SimpleHomelab" /></a> <a href="https://github.com/jmadden91"><img src="https://github.com/jmadden91.png" width="64px" alt="User avatar: jmadden91" /></a> <a href="https://github.com/tribor"><img src="https://github.com/tribor.png" width="64px" alt="User avatar: tribor" /></a> <a href="https://github.com/eliasbenb"><img src="https://github.com/eliasbenb.png" width="64px" alt="User avatar: eliasbenb" /></a> <a href="https://github.com/afunworm"><img src="https://github.com/afunworm.png" width="64px" alt="User avatar: afunworm" /></a> <a href="https://github.com/chip-well"><img src="https://github.com/chip-well.png" width="64px" alt="User avatar: chip-well" /></a> <a href="https://github.com/Lancelot-Enguerrand"><img src="https://github.com/Lancelot-Enguerrand.png" width="64px" alt="User avatar: Lancelot-Enguerrand" /></a> <a href="https://github.com/allgoewer"><img src="https://github.com/allgoewer.png" width="64px" alt="User avatar: allgoewer" /></a> <a href="https://github.com/NEANC"><img src="https://github.com/NEANC.png" width="64px" alt="User avatar: NEANC" /></a> <a href="https://github.com/ax-mad"><img src="https://github.com/ax-mad.png" width="64px" alt="User avatar: ax-mad" /></a> <a href="https://github.com/stegratech"><img src="https://github.com/stegratech.png" width="64px" alt="User avatar: stegratech" /></a> <!-- sponsors -->
|
||||||
|
|
||||||
## Acknowledgements
|
## Acknowledgements
|
||||||
|
|
||||||
|
|||||||
@@ -68,7 +68,10 @@ func generateTotpCmd() *cli.Command {
|
|||||||
return fmt.Errorf("failed to parse user: %w", err)
|
return fmt.Errorf("failed to parse user: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
docker := strings.Contains(tCfg.User, "$$")
|
docker := false
|
||||||
|
if strings.Contains(tCfg.User, "$$") {
|
||||||
|
docker = true
|
||||||
|
}
|
||||||
|
|
||||||
if user.TOTPSecret != "" {
|
if user.TOTPSecret != "" {
|
||||||
return fmt.Errorf("user already has a TOTP secret")
|
return fmt.Errorf("user already has a TOTP secret")
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/tinyauthapp/paerser/cli"
|
|
||||||
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
"github.com/tinyauthapp/tinyauth/internal/utils/tlog"
|
||||||
|
"github.com/tinyauthapp/paerser/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
type healthzResponse struct {
|
type healthzResponse struct {
|
||||||
@@ -45,7 +45,7 @@ func healthcheckCmd() *cli.Command {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if appUrl == "" {
|
if appUrl == "" {
|
||||||
return errors.New("could not determine app url")
|
return errors.New("Could not determine app URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check")
|
tlog.App.Info().Str("app_url", appUrl).Msg("Performing health check")
|
||||||
@@ -70,7 +70,7 @@ func healthcheckCmd() *cli.Command {
|
|||||||
return fmt.Errorf("service is not healthy, got: %s", resp.Status)
|
return fmt.Errorf("service is not healthy, got: %s", resp.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
defer resp.Body.Close() //nolint:errcheck
|
defer resp.Body.Close()
|
||||||
|
|
||||||
var healthResp healthzResponse
|
var healthResp healthzResponse
|
||||||
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ type BootstrapApp struct {
|
|||||||
oauthSessionCookieName string
|
oauthSessionCookieName string
|
||||||
localUsers *[]model.LocalUser
|
localUsers *[]model.LocalUser
|
||||||
oauthProviders map[string]model.OAuthServiceConfig
|
oauthProviders map[string]model.OAuthServiceConfig
|
||||||
oauthWhitelist []string
|
|
||||||
configuredProviders []controller.Provider
|
configuredProviders []controller.Provider
|
||||||
oidcClients []model.OIDCClientConfig
|
oidcClients []model.OIDCClientConfig
|
||||||
}
|
}
|
||||||
@@ -72,13 +71,6 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
|
|
||||||
app.context.localUsers = users
|
app.context.localUsers = users
|
||||||
|
|
||||||
oauthWhitelist, err := utils.GetStringList(app.config.OAuth.Whitelist, app.config.OAuth.WhitelistFile)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
app.context.oauthWhitelist = oauthWhitelist
|
|
||||||
|
|
||||||
// Setup OAuth providers
|
// Setup OAuth providers
|
||||||
app.context.oauthProviders = app.config.OAuth.Providers
|
app.context.oauthProviders = app.config.OAuth.Providers
|
||||||
|
|
||||||
@@ -112,13 +104,7 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get cookie domain
|
// Get cookie domain
|
||||||
cookieDomainResolver := utils.GetCookieDomain
|
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
|
||||||
if !app.config.Auth.SubdomainsEnabled {
|
|
||||||
tlog.App.Info().Msg("Subdomains disabled, automatic authentication for proxied apps will not work")
|
|
||||||
cookieDomainResolver = utils.GetStandaloneCookieDomain
|
|
||||||
}
|
|
||||||
|
|
||||||
cookieDomain, err := cookieDomainResolver(app.context.appUrl)
|
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -292,7 +278,7 @@ func (app *BootstrapApp) heartbeatRoutine() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
res.Body.Close() //nolint:errcheck
|
res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode != 200 && res.StatusCode != 201 {
|
if res.StatusCode != 200 && res.StatusCode != 201 {
|
||||||
tlog.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
|
tlog.App.Debug().Str("status", res.Status).Msg("Heartbeat returned non-200/201 status")
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ func (app *BootstrapApp) setupRouter() (*gin.Engine, error) {
|
|||||||
RedirectCookieName: app.context.redirectCookieName,
|
RedirectCookieName: app.context.redirectCookieName,
|
||||||
CookieDomain: app.context.cookieDomain,
|
CookieDomain: app.context.cookieDomain,
|
||||||
OAuthSessionCookieName: app.context.oauthSessionCookieName,
|
OAuthSessionCookieName: app.context.oauthSessionCookieName,
|
||||||
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
|
|
||||||
}, apiRouter, app.services.authService)
|
}, apiRouter, app.services.authService)
|
||||||
|
|
||||||
oauthController.SetupRoutes()
|
oauthController.SetupRoutes()
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to setup LDAP service, starting without it")
|
tlog.App.Warn().Err(err).Msg("Failed to setup LDAP service, starting without it")
|
||||||
ldapService.Unconfigure() //nolint:errcheck
|
ldapService.Unconfigure()
|
||||||
}
|
}
|
||||||
|
|
||||||
services.ldapService = ldapService
|
services.ldapService = ldapService
|
||||||
@@ -90,7 +90,7 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
|
|
||||||
authService := service.NewAuthService(service.AuthServiceConfig{
|
authService := service.NewAuthService(service.AuthServiceConfig{
|
||||||
LocalUsers: app.context.localUsers,
|
LocalUsers: app.context.localUsers,
|
||||||
OauthWhitelist: app.context.oauthWhitelist,
|
OauthWhitelist: app.config.OAuth.Whitelist,
|
||||||
SessionExpiry: app.config.Auth.SessionExpiry,
|
SessionExpiry: app.config.Auth.SessionExpiry,
|
||||||
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
|
SessionMaxLifetime: app.config.Auth.SessionMaxLifetime,
|
||||||
SecureCookie: app.config.Auth.SecureCookie,
|
SecureCookie: app.config.Auth.SecureCookie,
|
||||||
@@ -100,7 +100,6 @@ func (app *BootstrapApp) initServices(queries *repository.Queries) (Services, er
|
|||||||
SessionCookieName: app.context.sessionCookieName,
|
SessionCookieName: app.context.sessionCookieName,
|
||||||
IP: app.config.Auth.IP,
|
IP: app.config.Auth.IP,
|
||||||
LDAPGroupsCacheTTL: app.config.LDAP.GroupCacheTTL,
|
LDAPGroupsCacheTTL: app.config.LDAP.GroupCacheTTL,
|
||||||
SubdomainsEnabled: app.config.Auth.SubdomainsEnabled,
|
|
||||||
}, services.ldapService, queries, services.oauthBrokerService)
|
}, services.ldapService, queries, services.oauthBrokerService)
|
||||||
|
|
||||||
err = authService.Init()
|
err = authService.Init()
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type OAuthControllerConfig struct {
|
|||||||
SecureCookie bool
|
SecureCookie bool
|
||||||
AppURL string
|
AppURL string
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
SubdomainsEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthController struct {
|
type OAuthController struct {
|
||||||
@@ -106,7 +105,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.SecureCookie, true)
|
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -136,7 +135,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", controller.getCookieDomain(), controller.config.SecureCookie, true)
|
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
||||||
|
|
||||||
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
|
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
|
||||||
|
|
||||||
@@ -166,12 +165,6 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
|
|
||||||
user, err := controller.auth.GetOAuthUserinfo(sessionIdCookie)
|
user, err := controller.auth.GetOAuthUserinfo(sessionIdCookie)
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
tlog.App.Error().Err(err).Msg("Failed to get user info from OAuth provider")
|
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if user.Email == "" {
|
if user.Email == "" {
|
||||||
tlog.App.Error().Msg("OAuth provider did not return an email")
|
tlog.App.Error().Msg("OAuth provider did not return an email")
|
||||||
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
c.Redirect(http.StatusTemporaryRedirect, fmt.Sprintf("%s/error", controller.config.AppURL))
|
||||||
@@ -290,10 +283,3 @@ func (controller *OAuthController) isOidcRequest(params service.OAuthURLParams)
|
|||||||
params.ClientID != "" &&
|
params.ClientID != "" &&
|
||||||
params.RedirectURI != ""
|
params.RedirectURI != ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func (controller *OAuthController) getCookieDomain() string {
|
|
||||||
if controller.config.SubdomainsEnabled {
|
|
||||||
return "." + controller.config.CookieDomain
|
|
||||||
}
|
|
||||||
return controller.config.CookieDomain
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -179,7 +179,7 @@ func TestUserController(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "Should rate limit on 3 invalid attempts",
|
description: "Should rate limit on 3 invalid attempts",
|
||||||
middlewares: []gin.HandlerFunc{},
|
middlewares: []gin.HandlerFunc{},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { //nolint:staticcheck
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
loginReq := controller.LoginRequest{
|
loginReq := controller.LoginRequest{
|
||||||
Username: "testuser",
|
Username: "testuser",
|
||||||
Password: "wrongpassword",
|
Password: "wrongpassword",
|
||||||
@@ -201,7 +201,7 @@ func TestUserController(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4th attempt should be rate limited
|
// 4th attempt should be rate limited
|
||||||
recorder = httptest.NewRecorder() //nolint:staticcheck
|
recorder = httptest.NewRecorder()
|
||||||
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
|
req := httptest.NewRequest("POST", "/api/user/login", strings.NewReader(string(loginReqBody)))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
@@ -293,7 +293,7 @@ func TestUserController(t *testing.T) {
|
|||||||
middlewares: []gin.HandlerFunc{
|
middlewares: []gin.HandlerFunc{
|
||||||
totpCtx,
|
totpCtx,
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { //nolint:staticcheck
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
_, err := queries.CreateSession(context.TODO(), repository.CreateSessionParams{
|
_, err := queries.CreateSession(context.TODO(), repository.CreateSessionParams{
|
||||||
UUID: "test-totp-login-uuid",
|
UUID: "test-totp-login-uuid",
|
||||||
Username: "test",
|
Username: "test",
|
||||||
@@ -316,7 +316,7 @@ func TestUserController(t *testing.T) {
|
|||||||
totpReqBody, err := json.Marshal(totpReq)
|
totpReqBody, err := json.Marshal(totpReq)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
recorder = httptest.NewRecorder() //nolint:staticcheck
|
recorder = httptest.NewRecorder()
|
||||||
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
req.AddCookie(&http.Cookie{
|
req.AddCookie(&http.Cookie{
|
||||||
@@ -345,7 +345,7 @@ func TestUserController(t *testing.T) {
|
|||||||
middlewares: []gin.HandlerFunc{
|
middlewares: []gin.HandlerFunc{
|
||||||
totpCtx,
|
totpCtx,
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) { //nolint:staticcheck
|
run: func(t *testing.T, router *gin.Engine, recorder *httptest.ResponseRecorder) {
|
||||||
for range 3 {
|
for range 3 {
|
||||||
totpReq := controller.TotpRequest{
|
totpReq := controller.TotpRequest{
|
||||||
Code: "000000", // invalid code
|
Code: "000000", // invalid code
|
||||||
@@ -354,7 +354,7 @@ func TestUserController(t *testing.T) {
|
|||||||
totpReqBody, err := json.Marshal(totpReq)
|
totpReqBody, err := json.Marshal(totpReq)
|
||||||
assert.NoError(t, err)
|
assert.NoError(t, err)
|
||||||
|
|
||||||
recorder = httptest.NewRecorder() //nolint:staticcheck
|
recorder = httptest.NewRecorder()
|
||||||
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
req := httptest.NewRequest("POST", "/api/user/totp", strings.NewReader(string(totpReqBody)))
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !m.auth.IsEmailWhitelisted(userContext.OAuth.Email) {
|
if !m.auth.IsEmailWhitelisted(userContext.OAuth.Email) {
|
||||||
m.auth.DeleteSession(ctx, uuid) //nolint:errcheck
|
m.auth.DeleteSession(ctx, uuid)
|
||||||
return nil, nil, fmt.Errorf("email from session cookie not whitelisted: %s", userContext.OAuth.Email)
|
return nil, nil, fmt.Errorf("email from session cookie not whitelisted: %s", userContext.OAuth.Email)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ func NewDefaultConfiguration() *Config {
|
|||||||
Address: "0.0.0.0",
|
Address: "0.0.0.0",
|
||||||
},
|
},
|
||||||
Auth: AuthConfig{
|
Auth: AuthConfig{
|
||||||
SubdomainsEnabled: true,
|
|
||||||
SessionExpiry: 86400, // 1 day
|
SessionExpiry: 86400, // 1 day
|
||||||
SessionMaxLifetime: 0, // disabled
|
SessionMaxLifetime: 0, // disabled
|
||||||
LoginTimeout: 300, // 5 minutes
|
LoginTimeout: 300, // 5 minutes
|
||||||
@@ -103,7 +102,6 @@ type ServerConfig struct {
|
|||||||
type AuthConfig struct {
|
type AuthConfig struct {
|
||||||
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
IP IPConfig `description:"IP whitelisting config options." yaml:"ip"`
|
||||||
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
Users []string `description:"Comma-separated list of users (username:hashed_password)." yaml:"users"`
|
||||||
SubdomainsEnabled bool `description:"Enable subdomains support." yaml:"subdomainsEnabled"`
|
|
||||||
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
|
UserAttributes map[string]UserAttributes `description:"Map of per-user OIDC attributes (username -> attributes)." yaml:"userAttributes"`
|
||||||
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
UsersFile string `description:"Path to the users file." yaml:"usersFile"`
|
||||||
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
SecureCookie bool `description:"Enable secure cookies." yaml:"secureCookie"`
|
||||||
@@ -148,7 +146,6 @@ type IPConfig struct {
|
|||||||
|
|
||||||
type OAuthConfig struct {
|
type OAuthConfig struct {
|
||||||
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"`
|
||||||
WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile"`
|
|
||||||
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
|
AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"`
|
||||||
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -84,7 +84,6 @@ type AuthServiceConfig struct {
|
|||||||
SessionCookieName string
|
SessionCookieName string
|
||||||
IP model.IPConfig
|
IP model.IPConfig
|
||||||
LDAPGroupsCacheTTL int
|
LDAPGroupsCacheTTL int
|
||||||
SubdomainsEnabled bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type AuthService struct {
|
type AuthService struct {
|
||||||
@@ -398,12 +397,6 @@ func (auth *AuthService) DeleteSession(ctx context.Context, uuid string) (*http.
|
|||||||
tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway")
|
tlog.App.Warn().Err(err).Msg("Failed to delete session from database, proceeding to clear cookie anyway")
|
||||||
}
|
}
|
||||||
|
|
||||||
err = auth.queries.DeleteSession(ctx, uuid)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return &http.Cookie{
|
return &http.Cookie{
|
||||||
Name: auth.config.SessionCookieName,
|
Name: auth.config.SessionCookieName,
|
||||||
Value: "",
|
Value: "",
|
||||||
|
|||||||
@@ -269,7 +269,7 @@ func (ldap *LdapService) reconnect() error {
|
|||||||
exp.Reset()
|
exp.Reset()
|
||||||
|
|
||||||
operation := func() (*ldapgo.Conn, error) {
|
operation := func() (*ldapgo.Conn, error) {
|
||||||
ldap.conn.Close() //nolint:errcheck
|
ldap.conn.Close()
|
||||||
conn, err := ldap.connect()
|
conn, err := ldap.connect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ func simpleReq[T any](client *http.Client, url string, headers map[string]string
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
defer res.Body.Close() //nolint:errcheck
|
defer res.Body.Close()
|
||||||
|
|
||||||
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
if res.StatusCode < 200 || res.StatusCode >= 300 {
|
||||||
return nil, fmt.Errorf("request failed with status: %s", res.Status)
|
return nil, fmt.Errorf("request failed with status: %s", res.Status)
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ func GetCookieDomain(u string) (string, error) {
|
|||||||
host := parsed.Hostname()
|
host := parsed.Hostname()
|
||||||
|
|
||||||
if netIP := net.ParseIP(host); netIP != nil {
|
if netIP := net.ParseIP(host); netIP != nil {
|
||||||
return "", errors.New("ip addresses not allowed")
|
return "", errors.New("IP addresses not allowed")
|
||||||
}
|
}
|
||||||
|
|
||||||
parts := strings.Split(host, ".")
|
parts := strings.Split(host, ".")
|
||||||
@@ -47,27 +47,6 @@ func GetCookieDomain(u string) (string, error) {
|
|||||||
return domain, nil
|
return domain, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetStandaloneCookieDomain(u string) (string, error) {
|
|
||||||
parsed, err := url.Parse(u)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
host := parsed.Hostname()
|
|
||||||
|
|
||||||
if netIP := net.ParseIP(host); netIP != nil {
|
|
||||||
return "", errors.New("ip addresses not allowed")
|
|
||||||
}
|
|
||||||
|
|
||||||
parts := strings.Split(host, ".")
|
|
||||||
|
|
||||||
if len(parts) < 2 {
|
|
||||||
return "", errors.New("invalid app url")
|
|
||||||
}
|
|
||||||
|
|
||||||
return host, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseFileToLine(content string) string {
|
func ParseFileToLine(content string) string {
|
||||||
lines := strings.Split(content, "\n")
|
lines := strings.Split(content, "\n")
|
||||||
users := make([]string, 0)
|
users := make([]string, 0)
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func TestGetRootDomain(t *testing.T) {
|
|||||||
// IP address
|
// IP address
|
||||||
domain = "http://10.10.10.10"
|
domain = "http://10.10.10.10"
|
||||||
_, err = utils.GetCookieDomain(domain)
|
_, err = utils.GetCookieDomain(domain)
|
||||||
assert.ErrorContains(t, err, "ip addresses not allowed")
|
assert.ErrorContains(t, err, "IP addresses not allowed")
|
||||||
|
|
||||||
// Invalid URL
|
// Invalid URL
|
||||||
domain = "http://[::1]:namedport"
|
domain = "http://[::1]:namedport"
|
||||||
@@ -180,48 +180,3 @@ func TestIsRedirectSafe(t *testing.T) {
|
|||||||
result = utils.IsRedirectSafe(redirectURL, domain)
|
result = utils.IsRedirectSafe(redirectURL, domain)
|
||||||
assert.False(t, result)
|
assert.False(t, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestGetStandaloneCookieDomain(t *testing.T) {
|
|
||||||
// Normal case
|
|
||||||
domain := "http://tinyauth.app"
|
|
||||||
expected := "tinyauth.app"
|
|
||||||
result, err := utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, result)
|
|
||||||
|
|
||||||
// URL with subdomain (full hostname is returned, no subdomain stripping)
|
|
||||||
domain = "http://sub.tinyauth.app"
|
|
||||||
expected = "sub.tinyauth.app"
|
|
||||||
result, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, result)
|
|
||||||
|
|
||||||
// URL with port (port should be stripped)
|
|
||||||
domain = "http://tinyauth.app:8080"
|
|
||||||
expected = "tinyauth.app"
|
|
||||||
result, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, result)
|
|
||||||
|
|
||||||
// URL with path
|
|
||||||
domain = "https://tinyauth.app/some/path"
|
|
||||||
expected = "tinyauth.app"
|
|
||||||
result, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, expected, result)
|
|
||||||
|
|
||||||
// IP address
|
|
||||||
domain = "http://10.10.10.10"
|
|
||||||
_, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.ErrorContains(t, err, "ip addresses not allowed")
|
|
||||||
|
|
||||||
// Invalid domain (only TLD)
|
|
||||||
domain = "com"
|
|
||||||
_, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.ErrorContains(t, err, "invalid app url")
|
|
||||||
|
|
||||||
// Invalid URL
|
|
||||||
domain = "http://[::1]:namedport"
|
|
||||||
_, err = utils.GetStandaloneCookieDomain(domain)
|
|
||||||
assert.ErrorContains(t, err, "parse \"http://[::1]:namedport\": invalid port \":namedport\" after host")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ func TestReadFile(t *testing.T) {
|
|||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove("/tmp/tinyauth_test_file") //nolint:errcheck
|
defer os.Remove("/tmp/tinyauth_test_file")
|
||||||
|
|
||||||
// Normal case
|
// Normal case
|
||||||
content, err := ReadFile("/tmp/tinyauth_test_file")
|
content, err := ReadFile("/tmp/tinyauth_test_file")
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func FilterIP(filter string, ip string) (bool, error) {
|
|||||||
return false, errors.New("invalid IP address")
|
return false, errors.New("invalid IP address")
|
||||||
}
|
}
|
||||||
|
|
||||||
filter = strings.ReplaceAll(filter, "-", "/")
|
filter = strings.Replace(filter, "-", "/", -1)
|
||||||
|
|
||||||
if strings.Contains(filter, "/") {
|
if strings.Contains(filter, "/") {
|
||||||
_, cidr, err := net.ParseCIDR(filter)
|
_, cidr, err := net.ParseCIDR(filter)
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ func TestGetSecret(t *testing.T) {
|
|||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove("/tmp/tinyauth_test_secret") //nolint:errcheck
|
defer os.Remove("/tmp/tinyauth_test_secret")
|
||||||
|
|
||||||
// Get from config
|
// Get from config
|
||||||
assert.Equal(t, "mysecret", utils.GetSecret("mysecret", ""))
|
assert.Equal(t, "mysecret", utils.GetSecret("mysecret", ""))
|
||||||
|
|||||||
@@ -28,41 +28,3 @@ func CoalesceToString(value any) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ParseNonEmptyLines(contents string) []string {
|
|
||||||
lines := make([]string, 0)
|
|
||||||
|
|
||||||
for line := range strings.SplitSeq(contents, "\n") {
|
|
||||||
lineTrimmed := strings.TrimSpace(line)
|
|
||||||
if lineTrimmed == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
lines = append(lines, lineTrimmed)
|
|
||||||
}
|
|
||||||
|
|
||||||
return lines
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetStringList(valuesCfg []string, valuesPath string) ([]string, error) {
|
|
||||||
values := make([]string, 0, len(valuesCfg))
|
|
||||||
|
|
||||||
for _, value := range valuesCfg {
|
|
||||||
valueTrimmed := strings.TrimSpace(value)
|
|
||||||
if valueTrimmed == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
values = append(values, valueTrimmed)
|
|
||||||
}
|
|
||||||
|
|
||||||
if valuesPath == "" {
|
|
||||||
return values, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
contents, err := ReadFile(valuesPath)
|
|
||||||
if err != nil {
|
|
||||||
return []string{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
values = append(values, ParseNonEmptyLines(contents)...)
|
|
||||||
return values, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package utils_test
|
package utils_test
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"os"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
@@ -57,33 +56,3 @@ func TestCompileUserEmail(t *testing.T) {
|
|||||||
// Test with invalid email
|
// Test with invalid email
|
||||||
assert.Equal(t, "user@example.com", utils.CompileUserEmail("user", "example.com"))
|
assert.Equal(t, "user@example.com", utils.CompileUserEmail("user", "example.com"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestParseNonEmptyLines(t *testing.T) {
|
|
||||||
lines := utils.ParseNonEmptyLines(" first@example.com \n\n second@example.com \n \n")
|
|
||||||
|
|
||||||
assert.Equal(t, []string{"first@example.com", "second@example.com"}, lines)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestGetStringList(t *testing.T) {
|
|
||||||
file, err := os.Create("/tmp/tinyauth_list_test_file")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
_, err = file.WriteString(" third@example.com \n\n fourth@example.com \n")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
|
|
||||||
err = file.Close()
|
|
||||||
assert.NoError(t, err)
|
|
||||||
defer os.Remove("/tmp/tinyauth_list_test_file") //nolint:errcheck
|
|
||||||
|
|
||||||
values, err := utils.GetStringList([]string{" first@example.com ", "", "second@example.com"}, "/tmp/tinyauth_list_test_file")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, []string{"first@example.com", "second@example.com", "third@example.com", "fourth@example.com"}, values)
|
|
||||||
|
|
||||||
values, err = utils.GetStringList(nil, "")
|
|
||||||
assert.NoError(t, err)
|
|
||||||
assert.Equal(t, []string{}, values)
|
|
||||||
|
|
||||||
values, err = utils.GetStringList(nil, "/tmp/non_existing_list_file")
|
|
||||||
assert.ErrorContains(t, err, "no such file or directory")
|
|
||||||
assert.Equal(t, []string{}, values)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttribute
|
|||||||
var users []model.LocalUser
|
var users []model.LocalUser
|
||||||
|
|
||||||
if len(usersStr) == 0 {
|
if len(usersStr) == 0 {
|
||||||
return nil, nil
|
return &users, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, user := range usersStr {
|
for _, user := range usersStr {
|
||||||
@@ -34,9 +34,32 @@ func ParseUsers(usersStr []string, userAttributes map[string]model.UserAttribute
|
|||||||
}
|
}
|
||||||
|
|
||||||
func GetUsers(usersCfg []string, usersPath string, userAttributes map[string]model.UserAttributes) (*[]model.LocalUser, error) {
|
func GetUsers(usersCfg []string, usersPath string, userAttributes map[string]model.UserAttributes) (*[]model.LocalUser, error) {
|
||||||
usersStr, err := GetStringList(usersCfg, usersPath)
|
var usersStr []string
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
if len(usersCfg) == 0 && usersPath == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(usersCfg) > 0 {
|
||||||
|
usersStr = append(usersStr, usersCfg...)
|
||||||
|
}
|
||||||
|
|
||||||
|
if usersPath != "" {
|
||||||
|
contents, err := ReadFile(usersPath)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lines := strings.SplitSeq(contents, "\n")
|
||||||
|
|
||||||
|
for line := range lines {
|
||||||
|
lineTrimmed := strings.TrimSpace(line)
|
||||||
|
if lineTrimmed == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
usersStr = append(usersStr, lineTrimmed)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return ParseUsers(usersStr, userAttributes)
|
return ParseUsers(usersStr, userAttributes)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ func TestGetUsers(t *testing.T) {
|
|||||||
|
|
||||||
err = file.Close()
|
err = file.Close()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer os.Remove(tmpDir + "/tinyauth_users_test.txt") //nolint:errcheck
|
defer os.Remove(tmpDir + "/tinyauth_users_test.txt")
|
||||||
|
|
||||||
noAttrs := map[string]model.UserAttributes{}
|
noAttrs := map[string]model.UserAttributes{}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user