mirror of
https://github.com/steveiliop56/tinyauth.git
synced 2026-07-11 12:31:12 +00:00
Compare commits
8 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5aeb886523 | |||
| 1382ab41e7 | |||
| 24f2da4e58 | |||
| b06b60150f | |||
| 4077bacfdf | |||
| 4c0181c5e2 | |||
| 44a7cbf41b | |||
| d90e3d652d |
@@ -38,6 +38,6 @@ jobs:
|
|||||||
retention-days: 5
|
retention-days: 5
|
||||||
|
|
||||||
- name: Upload to code-scanning
|
- name: Upload to code-scanning
|
||||||
uses: github/codeql-action/upload-sarif@95e58e9a2cdfd71adc6e0353d5c52f41a045d225 # v4
|
uses: github/codeql-action/upload-sarif@e46ed2cbd01164d986452f91f178727624ae40d7 # v4
|
||||||
with:
|
with:
|
||||||
sarif_file: results.sarif
|
sarif_file: results.sarif
|
||||||
|
|||||||
@@ -104,7 +104,13 @@ func (app *BootstrapApp) Setup() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Get cookie domain
|
// Get cookie domain
|
||||||
cookieDomain, err := utils.GetCookieDomain(app.context.appUrl)
|
cookieDomainResolver := utils.GetCookieDomain
|
||||||
|
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
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ 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()
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ 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()
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ func (controller *ContextController) userContextHandler(c *gin.Context) {
|
|||||||
Username: context.GetUsername(),
|
Username: context.GetUsername(),
|
||||||
Name: context.GetName(),
|
Name: context.GetName(),
|
||||||
Email: context.GetEmail(),
|
Email: context.GetEmail(),
|
||||||
Provider: context.ProviderName(),
|
Provider: context.GetProviderID(),
|
||||||
OAuth: context.IsOAuth(),
|
OAuth: context.IsOAuth(),
|
||||||
TOTPPending: context.TOTPPending(),
|
TOTPPending: context.TOTPPending(),
|
||||||
OAuthName: context.OAuthName(),
|
OAuthName: context.OAuthName(),
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ type OAuthControllerConfig struct {
|
|||||||
SecureCookie bool
|
SecureCookie bool
|
||||||
AppURL string
|
AppURL string
|
||||||
CookieDomain string
|
CookieDomain string
|
||||||
|
SubdomainsEnabled bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type OAuthController struct {
|
type OAuthController struct {
|
||||||
@@ -105,7 +106,7 @@ func (controller *OAuthController) oauthURLHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
c.SetCookie(controller.config.OAuthSessionCookieName, sessionId, int(time.Hour.Seconds()), "/", controller.getCookieDomain(), controller.config.SecureCookie, true)
|
||||||
|
|
||||||
c.JSON(200, gin.H{
|
c.JSON(200, gin.H{
|
||||||
"status": 200,
|
"status": 200,
|
||||||
@@ -135,7 +136,7 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", fmt.Sprintf(".%s", controller.config.CookieDomain), controller.config.SecureCookie, true)
|
c.SetCookie(controller.config.OAuthSessionCookieName, "", -1, "/", controller.getCookieDomain(), controller.config.SecureCookie, true)
|
||||||
|
|
||||||
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
|
oauthPendingSession, err := controller.auth.GetOAuthPendingSession(sessionIdCookie)
|
||||||
|
|
||||||
@@ -283,3 +284,10 @@ 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
|
||||||
|
}
|
||||||
|
|||||||
@@ -98,7 +98,6 @@ func TestProxyController(t *testing.T) {
|
|||||||
Name: "Totpuser",
|
Name: "Totpuser",
|
||||||
Email: "totpuser@example.com",
|
Email: "totpuser@example.com",
|
||||||
},
|
},
|
||||||
TOTPEnabled: true,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
c.Next()
|
c.Next()
|
||||||
|
|||||||
@@ -249,7 +249,7 @@ func (controller *UserController) logoutHandler(c *gin.Context) {
|
|||||||
context, err := new(model.UserContext).NewFromGin(c)
|
context, err := new(model.UserContext).NewFromGin(c)
|
||||||
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
tlog.AuditLogout(c, context.GetUsername(), context.ProviderName())
|
tlog.AuditLogout(c, context.GetUsername(), context.GetProviderID())
|
||||||
} else {
|
} else {
|
||||||
tlog.App.Warn().Err(err).Msg("Failed to get user context for logout audit, proceeding without username")
|
tlog.App.Warn().Err(err).Msg("Failed to get user context for logout audit, proceeding without username")
|
||||||
tlog.AuditLogout(c, "unknown", "unknown")
|
tlog.AuditLogout(c, "unknown", "unknown")
|
||||||
|
|||||||
@@ -78,7 +78,6 @@ func TestUserController(t *testing.T) {
|
|||||||
Email: "totpuser@example.com",
|
Email: "totpuser@example.com",
|
||||||
},
|
},
|
||||||
TOTPPending: true,
|
TOTPPending: true,
|
||||||
TOTPEnabled: true,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -94,7 +93,6 @@ func TestUserController(t *testing.T) {
|
|||||||
Email: "bob@example.com",
|
Email: "bob@example.com",
|
||||||
},
|
},
|
||||||
TOTPPending: true,
|
TOTPPending: true,
|
||||||
TOTPEnabled: true,
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -152,7 +150,9 @@ 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, 9, cookie.MaxAge)
|
// 3 seconds should be more than enough for even slow test environments
|
||||||
|
assert.GreaterOrEqual(t, cookie.MaxAge, 7)
|
||||||
|
assert.LessOrEqual(t, cookie.MaxAge, 10)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -241,7 +241,8 @@ 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, 3599, cookie.MaxAge) // 1 hour, default for totp pending sessions
|
assert.GreaterOrEqual(t, cookie.MaxAge, 3597)
|
||||||
|
assert.LessOrEqual(t, cookie.MaxAge, 3600)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -335,7 +336,8 @@ 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, 9, totpCookie.MaxAge) // should use the regular session expiry time
|
assert.GreaterOrEqual(t, totpCookie.MaxAge, 7)
|
||||||
|
assert.LessOrEqual(t, totpCookie.MaxAge, 10)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -123,7 +123,6 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
|
|||||||
|
|
||||||
if userContext.Provider == model.ProviderLocal &&
|
if userContext.Provider == model.ProviderLocal &&
|
||||||
userContext.Local.TOTPPending {
|
userContext.Local.TOTPPending {
|
||||||
userContext.Local.TOTPEnabled = true
|
|
||||||
return userContext, nil, nil
|
return userContext, nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -109,7 +109,6 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
assert.Equal(t, "testuser", userCtx.GetUsername())
|
assert.Equal(t, "testuser", userCtx.GetUsername())
|
||||||
assert.True(t, userCtx.Authenticated)
|
assert.True(t, userCtx.Authenticated)
|
||||||
require.NotNil(t, userCtx.Local)
|
require.NotNil(t, userCtx.Local)
|
||||||
assert.False(t, userCtx.Local.TOTPEnabled)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -134,7 +133,6 @@ func TestContextMiddleware(t *testing.T) {
|
|||||||
assert.False(t, userCtx.Authenticated)
|
assert.False(t, userCtx.Authenticated)
|
||||||
require.NotNil(t, userCtx.Local)
|
require.NotNil(t, userCtx.Local)
|
||||||
assert.True(t, userCtx.Local.TOTPPending)
|
assert.True(t, userCtx.Local.TOTPPending)
|
||||||
assert.True(t, userCtx.Local.TOTPEnabled)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ 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
|
||||||
@@ -102,6 +103,7 @@ 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"`
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ type BaseContext struct {
|
|||||||
type LocalContext struct {
|
type LocalContext struct {
|
||||||
BaseContext
|
BaseContext
|
||||||
TOTPPending bool
|
TOTPPending bool
|
||||||
TOTPEnabled bool
|
|
||||||
Attributes UserAttributes
|
Attributes UserAttributes
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,14 +222,14 @@ func (c *UserContext) GetName() string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UserContext) ProviderName() string {
|
func (c *UserContext) GetProviderID() string {
|
||||||
switch c.Provider {
|
switch c.Provider {
|
||||||
case ProviderBasicAuth, ProviderLocal:
|
case ProviderBasicAuth, ProviderLocal:
|
||||||
return "local"
|
return "local"
|
||||||
case ProviderLDAP:
|
case ProviderLDAP:
|
||||||
return "ldap"
|
return "ldap"
|
||||||
case ProviderOAuth:
|
case ProviderOAuth:
|
||||||
return c.OAuth.DisplayName // compatability
|
return c.OAuth.ID
|
||||||
default:
|
default:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -153,29 +153,29 @@ func TestContext(t *testing.T) {
|
|||||||
{
|
{
|
||||||
description: "ProviderName returns 'local' for ProviderLocal",
|
description: "ProviderName returns 'local' for ProviderLocal",
|
||||||
context: &model.UserContext{Provider: model.ProviderLocal},
|
context: &model.UserContext{Provider: model.ProviderLocal},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.ProviderName() },
|
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
||||||
expected: "local",
|
expected: "local",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "ProviderName returns 'local' for ProviderBasicAuth",
|
description: "ProviderName returns 'local' for ProviderBasicAuth",
|
||||||
context: &model.UserContext{Provider: model.ProviderBasicAuth},
|
context: &model.UserContext{Provider: model.ProviderBasicAuth},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.ProviderName() },
|
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
||||||
expected: "local",
|
expected: "local",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "ProviderName returns 'ldap' for ProviderLDAP",
|
description: "ProviderName returns 'ldap' for ProviderLDAP",
|
||||||
context: &model.UserContext{Provider: model.ProviderLDAP},
|
context: &model.UserContext{Provider: model.ProviderLDAP},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.ProviderName() },
|
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
||||||
expected: "ldap",
|
expected: "ldap",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "ProviderName returns OAuth DisplayName for ProviderOAuth",
|
description: "ProviderName returns OAuth provider ID for ProviderOAuth",
|
||||||
context: &model.UserContext{
|
context: &model.UserContext{
|
||||||
Provider: model.ProviderOAuth,
|
Provider: model.ProviderOAuth,
|
||||||
OAuth: &model.OAuthContext{DisplayName: "GitHub"},
|
OAuth: &model.OAuthContext{ID: "github"},
|
||||||
},
|
},
|
||||||
run: func(t *testing.T, c *model.UserContext) any { return c.ProviderName() },
|
run: func(t *testing.T, c *model.UserContext) any { return c.GetProviderID() },
|
||||||
expected: "GitHub",
|
expected: "github",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: "TOTPPending returns true when local context is pending",
|
description: "TOTPPending returns true when local context is pending",
|
||||||
|
|||||||
@@ -84,6 +84,7 @@ 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 {
|
||||||
@@ -397,6 +398,12 @@ 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: "",
|
||||||
@@ -838,3 +845,10 @@ func (auth *AuthService) ClearRateLimitsTestingOnly() {
|
|||||||
}
|
}
|
||||||
auth.loginMutex.Unlock()
|
auth.loginMutex.Unlock()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (auth *AuthService) getCookieDomain() string {
|
||||||
|
if auth.config.SubdomainsEnabled {
|
||||||
|
return "." + auth.config.CookieDomain
|
||||||
|
}
|
||||||
|
return auth.config.CookieDomain
|
||||||
|
}
|
||||||
|
|||||||
@@ -47,6 +47,15 @@ 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
|
||||||
|
}
|
||||||
|
|
||||||
|
return parsed.Hostname(), 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)
|
||||||
|
|||||||
Reference in New Issue
Block a user