From c42c0839235c4a4f4b34219b06adf78b9c918049 Mon Sep 17 00:00:00 2001 From: Mathieu Lemay Date: Sat, 23 May 2026 23:25:35 -0400 Subject: [PATCH 1/4] [WIP] feat: Add support for custom username overrides --- internal/bootstrap/app_bootstrap.go | 18 ++++++++++++++++++ internal/controller/oauth_controller.go | 6 +++++- internal/model/config.go | 10 ++++++---- internal/model/runtime.go | 1 + internal/service/auth_service.go | 5 +++++ 5 files changed, 35 insertions(+), 5 deletions(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index 92b049ef..e6a401cb 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -113,6 +113,24 @@ func (app *BootstrapApp) Setup() error { app.runtime.OAuthWhitelist = oauthWhitelist + // load oauth username overrides + oauthUsernameOverrides, err := utils.GetStringList(app.config.OAuth.UsernameOverrides, app.config.OAuth.UsernameOverridesFile) + + if err != nil { + return fmt.Errorf("failed to load oauth username overrides: %w", err) + } + + oauthUsernameOverrideMap := make(map[string]string, len(oauthUsernameOverrides)) + for _, override := range oauthUsernameOverrides { + parts := strings.SplitN(override, "=", 2) + if len(parts) != 2 { + return fmt.Errorf("invalid oauth username override format: %s", override) + } + oauthUsernameOverrideMap[parts[0]] = parts[1] + } + + app.runtime.OAuthUsernameOverrides = oauthUsernameOverrideMap + // setup oauth providers app.runtime.OAuthProviders = app.config.OAuth.Providers diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index e72c09fd..11de8433 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -218,7 +218,11 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { var username string - if strings.TrimSpace(user.PreferredUsername) != "" { + override, exists := controller.auth.GetUsernameOverride(user.Email) + if exists { + controller.log.App.Debug().Msg("Using username override from OAuth config") + username = override + } else if strings.TrimSpace(user.PreferredUsername) != "" { controller.log.App.Debug().Msg("Using preferred username from OAuth provider") username = user.PreferredUsername } else { diff --git a/internal/model/config.go b/internal/model/config.go index 5963e431..930981fb 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -160,10 +160,12 @@ type IPConfig struct { } type OAuthConfig struct { - 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"` - Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"` + Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"` + WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile"` + UsernameOverrides []string `description:"Comma-separated list of usernames to override." yaml:"usernameOverrides"` + UsernameOverridesFile string `description:"Path to the OAuth username overrides file." yaml:"usernameOverridesFile"` + AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"` + Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"` } type OIDCConfig struct { diff --git a/internal/model/runtime.go b/internal/model/runtime.go index 9df20b85..a5722550 100644 --- a/internal/model/runtime.go +++ b/internal/model/runtime.go @@ -11,6 +11,7 @@ type RuntimeConfig struct { LocalUsers []LocalUser OAuthProviders map[string]OAuthServiceConfig OAuthWhitelist []string + OAuthUsernameOverrides map[string]string ConfiguredProviders []Provider OIDCClients []OIDCClientConfig TrustedDomains []string diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 76fdafbd..300f9c4a 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -294,6 +294,11 @@ func (auth *AuthService) IsEmailWhitelisted(email string) bool { return match } +func (auth *AuthService) GetUsernameOverride(email string) (string, bool) { + username, exists := auth.runtime.OAuthUsernameOverrides[email] + return username, exists +} + func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) { if data.Provider == "tailscale" && auth.tailscale == nil { return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user") From 160c8be729b5b4f5c41d049eb64b798eef6339bd Mon Sep 17 00:00:00 2001 From: Mathieu Lemay Date: Mon, 25 May 2026 14:51:29 -0400 Subject: [PATCH 2/4] Use existing UserAttributes --- internal/bootstrap/app_bootstrap.go | 18 ----------- internal/controller/oauth_controller.go | 19 +++++++++--- internal/model/config.go | 41 ++++++++++++------------- internal/model/runtime.go | 1 - internal/service/auth_service.go | 5 --- 5 files changed, 34 insertions(+), 50 deletions(-) diff --git a/internal/bootstrap/app_bootstrap.go b/internal/bootstrap/app_bootstrap.go index a654b132..0bdd2214 100644 --- a/internal/bootstrap/app_bootstrap.go +++ b/internal/bootstrap/app_bootstrap.go @@ -113,24 +113,6 @@ func (app *BootstrapApp) Setup() error { app.runtime.OAuthWhitelist = oauthWhitelist - // load oauth username overrides - oauthUsernameOverrides, err := utils.GetStringList(app.config.OAuth.UsernameOverrides, app.config.OAuth.UsernameOverridesFile) - - if err != nil { - return fmt.Errorf("failed to load oauth username overrides: %w", err) - } - - oauthUsernameOverrideMap := make(map[string]string, len(oauthUsernameOverrides)) - for _, override := range oauthUsernameOverrides { - parts := strings.SplitN(override, "=", 2) - if len(parts) != 2 { - return fmt.Errorf("invalid oauth username override format: %s", override) - } - oauthUsernameOverrideMap[parts[0]] = parts[1] - } - - app.runtime.OAuthUsernameOverrides = oauthUsernameOverrideMap - // setup oauth providers app.runtime.OAuthProviders = app.config.OAuth.Providers diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index bab76442..657fed76 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -215,9 +215,14 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { return } + userAttribs := controller.getUserAttributes(user.Email) + var name string - if strings.TrimSpace(user.Name) != "" { + if userAttribs.Name != "" { + controller.log.App.Debug().Msg("Using name from Auth user attributes") + name = userAttribs.Name + } else if strings.TrimSpace(user.Name) != "" { controller.log.App.Debug().Msg("Using name from OAuth provider") name = user.Name } else { @@ -232,10 +237,9 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { var username string - override, exists := controller.auth.GetUsernameOverride(user.Email) - if exists { - controller.log.App.Debug().Msg("Using username override from OAuth config") - username = override + if userAttribs.PreferredUsername != "" { + controller.log.App.Debug().Msg("Using preferred username from Auth user attributes") + username = userAttribs.PreferredUsername } else if strings.TrimSpace(user.PreferredUsername) != "" { controller.log.App.Debug().Msg("Using preferred username from OAuth provider") username = user.PreferredUsername @@ -311,3 +315,8 @@ func (controller *OAuthController) getCookieDomain() string { } return controller.runtime.CookieDomain } + +func (controller *OAuthController) getUserAttributes(email string) model.UserAttributes { + attribs := controller.config.Auth.UserAttributes[email] + return attribs +} diff --git a/internal/model/config.go b/internal/model/config.go index 34a52045..e2275c57 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -127,21 +127,22 @@ type AuthConfig struct { } type UserAttributes struct { - Name string `description:"Full name of the user." yaml:"name"` - GivenName string `description:"Given (first) name of the user." yaml:"givenName"` - FamilyName string `description:"Family (last) name of the user." yaml:"familyName"` - MiddleName string `description:"Middle name of the user." yaml:"middleName"` - Nickname string `description:"Nickname of the user." yaml:"nickname"` - Profile string `description:"URL of the user's profile page." yaml:"profile"` - Picture string `description:"URL of the user's profile picture." yaml:"picture"` - Website string `description:"URL of the user's website." yaml:"website"` - Email string `description:"Email address of the user." yaml:"email"` - Gender string `description:"Gender of the user." yaml:"gender"` - Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate"` - Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo"` - Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale"` - PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber"` - Address AddressClaim `description:"Address of the user." yaml:"address"` + Name string `description:"Full name of the user." yaml:"name"` + GivenName string `description:"Given (first) name of the user." yaml:"givenName"` + FamilyName string `description:"Family (last) name of the user." yaml:"familyName"` + MiddleName string `description:"Middle name of the user." yaml:"middleName"` + Nickname string `description:"Nickname of the user." yaml:"nickname"` + PreferredUsername string `description:"Preferred username of the user." yaml:"preferredUsername"` + Profile string `description:"URL of the user's profile page." yaml:"profile"` + Picture string `description:"URL of the user's profile picture." yaml:"picture"` + Website string `description:"URL of the user's website." yaml:"website"` + Email string `description:"Email address of the user." yaml:"email"` + Gender string `description:"Gender of the user." yaml:"gender"` + Birthdate string `description:"Birthdate of the user (YYYY-MM-DD)." yaml:"birthdate"` + Zoneinfo string `description:"Time zone of the user (e.g. Europe/Athens)." yaml:"zoneinfo"` + Locale string `description:"Locale of the user (e.g. en-US)." yaml:"locale"` + PhoneNumber string `description:"Phone number of the user." yaml:"phoneNumber"` + Address AddressClaim `description:"Address of the user." yaml:"address"` } type AddressClaim struct { @@ -160,12 +161,10 @@ type IPConfig struct { } type OAuthConfig struct { - Whitelist []string `description:"Comma-separated list of allowed OAuth domains." yaml:"whitelist"` - WhitelistFile string `description:"Path to the OAuth whitelist file." yaml:"whitelistFile"` - UsernameOverrides []string `description:"Comma-separated list of usernames to override." yaml:"usernameOverrides"` - UsernameOverridesFile string `description:"Path to the OAuth username overrides file." yaml:"usernameOverridesFile"` - AutoRedirect string `description:"The OAuth provider to use for automatic redirection." yaml:"autoRedirect"` - Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"` + 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"` + Providers map[string]OAuthServiceConfig `description:"OAuth providers configuration." yaml:"providers"` } type OIDCConfig struct { diff --git a/internal/model/runtime.go b/internal/model/runtime.go index a5722550..9df20b85 100644 --- a/internal/model/runtime.go +++ b/internal/model/runtime.go @@ -11,7 +11,6 @@ type RuntimeConfig struct { LocalUsers []LocalUser OAuthProviders map[string]OAuthServiceConfig OAuthWhitelist []string - OAuthUsernameOverrides map[string]string ConfiguredProviders []Provider OIDCClients []OIDCClientConfig TrustedDomains []string diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index ec4b73af..5af7aa87 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -299,11 +299,6 @@ func (auth *AuthService) IsEmailWhitelisted(provider string, email string) bool return match } -func (auth *AuthService) GetUsernameOverride(email string) (string, bool) { - username, exists := auth.runtime.OAuthUsernameOverrides[email] - return username, exists -} - func (auth *AuthService) CreateSession(ctx context.Context, data repository.Session) (*http.Cookie, error) { if data.Provider == "tailscale" && auth.tailscale == nil { return nil, fmt.Errorf("tailscale service not configured, cannot create session for tailscale user") From b05f3a2affced9d245736df0e49f60d388e696b0 Mon Sep 17 00:00:00 2001 From: Mathieu Lemay Date: Mon, 25 May 2026 14:51:52 -0400 Subject: [PATCH 3/4] Add support for groups --- internal/controller/oauth_controller.go | 12 +++++++++++- internal/model/config.go | 1 + 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 657fed76..6f8d9812 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -248,12 +248,22 @@ func (controller *OAuthController) oauthCallbackHandler(c *gin.Context) { username = strings.Replace(user.Email, "@", "_", 1) } + var groups string + + if userAttribs.Groups != nil { + groups = strings.Join(userAttribs.Groups, ",") + controller.log.App.Debug().Msgf("Using groups from Auth user attributes: %s", groups) + } else { + controller.log.App.Debug().Msg("Using groups from OAuth provider") + groups = utils.CoalesceToString(user.Groups) + } + sessionCookie := repository.Session{ Username: username, Name: name, Email: user.Email, Provider: svc.ID(), - OAuthGroups: utils.CoalesceToString(user.Groups), + OAuthGroups: groups, OAuthName: svc.Name(), OAuthSub: user.Sub, } diff --git a/internal/model/config.go b/internal/model/config.go index e2275c57..96a5e78d 100644 --- a/internal/model/config.go +++ b/internal/model/config.go @@ -133,6 +133,7 @@ type UserAttributes struct { MiddleName string `description:"Middle name of the user." yaml:"middleName"` Nickname string `description:"Nickname of the user." yaml:"nickname"` PreferredUsername string `description:"Preferred username of the user." yaml:"preferredUsername"` + Groups []string `description:"List of groups the user belongs to." yaml:"groups"` Profile string `description:"URL of the user's profile page." yaml:"profile"` Picture string `description:"URL of the user's profile picture." yaml:"picture"` Website string `description:"URL of the user's website." yaml:"website"` From 3009cc5aa7deadc1b6935bce5454e4e290cc97fe Mon Sep 17 00:00:00 2001 From: Mathieu Lemay Date: Mon, 25 May 2026 15:20:11 -0400 Subject: [PATCH 4/4] Make email safe for env var parsing --- internal/controller/oauth_controller.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/internal/controller/oauth_controller.go b/internal/controller/oauth_controller.go index 6f8d9812..7cde7344 100644 --- a/internal/controller/oauth_controller.go +++ b/internal/controller/oauth_controller.go @@ -327,6 +327,8 @@ func (controller *OAuthController) getCookieDomain() string { } func (controller *OAuthController) getUserAttributes(email string) model.UserAttributes { + email = strings.ReplaceAll(email, "@", "-") + email = strings.ReplaceAll(email, ".", "-") attribs := controller.config.Auth.UserAttributes[email] return attribs }