From 5b516d3e187098264a84b8a3c104d19a3c9650de Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 19 Jul 2026 14:21:12 +0300 Subject: [PATCH 1/3] feat: experimental option to run tinyauth in oauth bridge mode --- cmd/tinyauth/tinyauth.go | 9 +-- internal/controller/oauth_controller.go | 82 +++++++++++++++-------- internal/middleware/context_middleware.go | 16 +++-- internal/model/config.go | 10 +-- 4 files changed, 77 insertions(+), 40 deletions(-) diff --git a/cmd/tinyauth/tinyauth.go b/cmd/tinyauth/tinyauth.go index b5720b0c..506cb0ca 100644 --- a/cmd/tinyauth/tinyauth.go +++ b/cmd/tinyauth/tinyauth.go @@ -3,6 +3,7 @@ package main import ( "fmt" "os" + "reflect" "strings" "charm.land/huh/v2" @@ -32,10 +33,10 @@ func main() { Resources: loaders, Run: func(_ []string) error { // enable this on experimental features - //if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) { - // colors := getColors() - // fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.") - //} + if !reflect.DeepEqual(model.NewDefaultConfiguration(env).Experimental, tConfig.Experimental) { + colors := getColors() + fmt.Println(colors.yellow.Render("⚠") + " Experimental features are enabled, use with caution. Experimental features may change with each release.") + } return runCmd(*tConfig) }, } diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 66756a82..6a61527e 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -220,35 +220,16 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { return } - var name string - - if strings.TrimSpace(user.Name) != "" { - controller.log.App.Debug().Msg("Using name from OAuth provider") - name = user.Name - } else { - controller.log.App.Debug().Msg("No name from OAuth provider, generating from email") - parts := strings.SplitN(user.Email, "@", 2) - if len(parts) == 2 { - name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1]) - } else { - name = utils.Capitalize(user.Email) - } - } - - var username string - - if strings.TrimSpace(user.PreferredUsername) != "" { - controller.log.App.Debug().Msg("Using preferred username from OAuth provider") - username = user.PreferredUsername - } else { - controller.log.App.Debug().Msg("No preferred username from OAuth provider, generating from email") - username = strings.Replace(user.Email, "@", "_", 1) - } + oauthUserInfo := controller.createOAuthUserInfo(oauthUserInfo{ + Username: user.PreferredUsername, + Email: user.Email, + Name: user.Name, + }) sessionCookie := repository.Session{ - Username: username, - Name: name, - Email: user.Email, + Username: oauthUserInfo.Username, + Name: oauthUserInfo.Name, + Email: oauthUserInfo.Email, Provider: svc.ID(), OAuthGroups: utils.CoalesceToString(user.Groups), OAuthName: svc.Name(), @@ -355,3 +336,50 @@ func (controller *OAuthController) isRedirectSafe(redirectURI string) bool { return false } + +type oauthUserInfo struct { + Email string + Username string + Name string +} + +func (controller *OAuthController) createOAuthUserInfo(input oauthUserInfo) oauthUserInfo { + info := oauthUserInfo{ + Email: input.Email, + } + + if controller.config.Experimental.OAuthBridgeEnabled { + if input.Username != "" { + info.Username = input.Username + } else { + info.Username = strings.SplitN(input.Email, "@", 2)[0] + } + + if input.Name != "" { + info.Name = input.Name + } else { + info.Name = utils.Capitalize(info.Username) + } + + return info + } + + if input.Name == "" { + controller.log.App.Debug().Msg("Using name from OAuth provider") + info.Name = input.Name + } else { + controller.log.App.Debug().Msg("No name from OAuth provider, generating from email") + parts := strings.SplitN(input.Email, "@", 2) + info.Name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1]) + } + + if input.Username != "" { + controller.log.App.Debug().Msg("Using preferred username from OAuth provider") + info.Username = input.Username + } else { + controller.log.App.Debug().Msg("No preferred username from OAuth provider, generating from email") + info.Username = strings.Replace(info.Email, "@", "_", 1) + } + + return info +} diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index ed0e808a..3b3ef150 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -40,6 +40,7 @@ var ( type ContextMiddleware struct { log *logger.Logger runtime *model.RuntimeConfig + config *model.Config auth *service.AuthService broker *service.OAuthBrokerService tailscale *service.TailscaleService @@ -50,6 +51,7 @@ type ContextMiddlewareInput struct { Log *logger.Logger RuntimeConfig *model.RuntimeConfig + StaticConfig *model.Config AuthService *service.AuthService BrokerService *service.OAuthBrokerService TailscaleService *service.TailscaleService @@ -59,6 +61,7 @@ func NewContextMiddleware(i ContextMiddlewareInput) *ContextMiddleware { return &ContextMiddleware{ log: i.Log, runtime: i.RuntimeConfig, + config: i.StaticConfig, auth: i.AuthService, broker: i.BrokerService, tailscale: i.TailscaleService, @@ -332,16 +335,19 @@ func (m *ContextMiddleware) tailscaleWhois(ip string) (*model.TailscaleContext, return nil, nil } - username := strings.Replace(whois.LoginName, "@", "_", 1) - uctx := model.TailscaleContext{ BaseContext: model.BaseContext{ - Username: username, - Email: whois.LoginName, - Name: whois.DisplayName, + Email: whois.LoginName, + Name: whois.DisplayName, }, NodeName: whois.NodeName, } + if m.config.Experimental.OAuthBridgeEnabled { + uctx.BaseContext.Username = strings.SplitN(whois.LoginName, "@", 2)[0] + } else { + uctx.BaseContext.Username = strings.Replace(whois.LoginName, "@", "_", 1) + } + return &uctx, nil } diff --git a/internal/model/config.go b/internal/model/config.go index 79e79788..c61eadb8 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -115,9 +115,9 @@ type Config struct { UI UIConfig `description:"UI customization." yaml:"ui,omitempty"` LDAP LDAPConfig `description:"LDAP configuration." yaml:"ldap,omitempty"` // enable the cli warning on experimental features - //Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"` - Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"` - Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"` + Experimental ExperimentalConfig `description:"Experimental features, use with caution." yaml:"experimental,omitempty"` + Tailscale TailscaleConfig `description:"Tailscale configuration." yaml:"tailscale,omitempty"` + Log LogConfig `description:"Logging configuration." yaml:"log,omitempty"` } type DatabaseConfig struct { @@ -238,7 +238,9 @@ type LogStreamConfig struct { Level string `description:"Log level for this stream. Use global if empty." yaml:"level,omitempty"` } -//type ExperimentalConfig struct{} +type ExperimentalConfig struct { + OAuthBridgeEnabled bool `description:"Enable the OAuth bridge, uses a new way to format OAuth user information." yaml:"oauthBridgeEnabled,omitempty"` +} type TailscaleConfig struct { Enabled bool `description:"Enable Tailscale integration." yaml:"enabled,omitempty"` From 00b57df3aebce4e75fb3da3d07173cde4dd24877 Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 19 Jul 2026 19:20:55 +0300 Subject: [PATCH 2/3] chore: update codegen --- .env.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.example b/.env.example index 6b02b52a..a06bee82 100644 --- a/.env.example +++ b/.env.example @@ -223,6 +223,11 @@ TINYAUTH_LDAP_AUTHKEY= # Cache duration for LDAP group membership in seconds. TINYAUTH_LDAP_GROUPCACHETTL=900 +# experimental config + +# Enable the OAuth bridge, uses a new way to format OAuth user information. +TINYAUTH_EXPERIMENTAL_OAUTHBRIDGEENABLED=false + # tailscale config # Enable Tailscale integration. From d10ff04e006ceac1938e0942f5a8ef4498e173df Mon Sep 17 00:00:00 2001 From: Stavros Date: Sun, 19 Jul 2026 19:30:31 +0300 Subject: [PATCH 3/3] fix: rabbit comments --- internal/controller/oauth_controller.go | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 6a61527e..fd6c2658 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -352,7 +352,12 @@ func (controller *OAuthController) createOAuthUserInfo(input oauthUserInfo) oaut if input.Username != "" { info.Username = input.Username } else { - info.Username = strings.SplitN(input.Email, "@", 2)[0] + parts := strings.SplitN(input.Email, "@", 2) + if len(parts) != 2 { + controller.log.App.Error().Str("email", input.Email).Msg("Invalid email address") + } else { + info.Username = parts[0] + } } if input.Name != "" { @@ -364,13 +369,17 @@ func (controller *OAuthController) createOAuthUserInfo(input oauthUserInfo) oaut return info } - if input.Name == "" { + if input.Name != "" { controller.log.App.Debug().Msg("Using name from OAuth provider") info.Name = input.Name } else { controller.log.App.Debug().Msg("No name from OAuth provider, generating from email") parts := strings.SplitN(input.Email, "@", 2) - info.Name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1]) + if len(parts) != 2 { + controller.log.App.Error().Str("email", input.Email).Msg("Invalid email address") + } else { + info.Name = fmt.Sprintf("%s (%s)", utils.Capitalize(parts[0]), parts[1]) + } } if input.Username != "" {