From d5cf076444db1864ec5762093db0943b709098e5 Mon Sep 17 00:00:00 2001 From: Ilyas Date: Sun, 3 May 2026 18:03:18 +0200 Subject: [PATCH 1/2] fix(ldap): pass through LDAP mail attribute instead of crafting email TinyAuth was constructing LDAP user emails as username@CookieDomain instead of using the mail attribute stored in the directory. This caused OIDC clients like Grafana to receive a synthetic email rather than the real one. Rename GetUserDN to GetUserInfo and extend it to also fetch the mail attribute in the same LDAP query. Thread the result through UserSearch and use it in both the login flow and the basic auth middleware, falling back to the crafted email only when LDAP returns no mail value. Co-Authored-By: Claude Sonnet 4.6 --- internal/config/config.go | 1 + internal/controller/user_controller.go | 3 +++ internal/middleware/context_middleware.go | 7 ++++++- internal/service/auth_service.go | 3 ++- internal/service/ldap_service.go | 13 ++++++------- 5 files changed, 18 insertions(+), 9 deletions(-) diff --git a/internal/config/config.go b/internal/config/config.go index e364b458..b0dcf2b2 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -268,6 +268,7 @@ type LdapUser struct { type UserSearch struct { Username string Type string // local, ldap or unknown + Email string } type UserContext struct { diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 187b33b9..2277c959 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -172,6 +172,9 @@ func (controller *UserController) loginHandler(c *gin.Context) { if userSearch.Type == "ldap" { sessionCookie.Provider = "ldap" + if userSearch.Email != "" { + sessionCookie.Email = userSearch.Email + } } tlog.App.Trace().Interface("session_cookie", sessionCookie).Msg("Creating session cookie") diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 651d9d85..27ab72e7 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -240,10 +240,15 @@ func (m *ContextMiddleware) Middleware() gin.HandlerFunc { return } + email := utils.CompileUserEmail(basic.Username, m.config.CookieDomain) + if userSearch.Email != "" { + email = userSearch.Email + } + c.Set("context", &config.UserContext{ Username: basic.Username, Name: utils.Capitalize(basic.Username), - Email: utils.CompileUserEmail(basic.Username, m.config.CookieDomain), + Email: email, Provider: "ldap", IsLoggedIn: true, LdapGroups: strings.Join(ldapUser.Groups, ","), diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index 0311229d..332c7efd 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -123,7 +123,7 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch { } if auth.ldap.IsConfigured() { - userDN, err := auth.ldap.GetUserDN(username) + userDN, email, err := auth.ldap.GetUserInfo(username) if err != nil { tlog.App.Warn().Err(err).Str("username", username).Msg("Failed to search for user in LDAP") @@ -135,6 +135,7 @@ func (auth *AuthService) SearchUser(username string) config.UserSearch { return config.UserSearch{ Username: userDN, Type: "ldap", + Email: email, } } diff --git a/internal/service/ldap_service.go b/internal/service/ldap_service.go index 0963ebf5..9d2ffdae 100644 --- a/internal/service/ldap_service.go +++ b/internal/service/ldap_service.go @@ -143,8 +143,7 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) { return ldap.conn, nil } -func (ldap *LdapService) GetUserDN(username string) (string, error) { - // Escape the username to prevent LDAP injection +func (ldap *LdapService) GetUserInfo(username string) (dn string, email string, err error) { escapedUsername := ldapgo.EscapeFilter(username) filter := fmt.Sprintf(ldap.config.SearchFilter, escapedUsername) @@ -152,7 +151,7 @@ func (ldap *LdapService) GetUserDN(username string) (string, error) { ldap.config.BaseDN, ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false, filter, - []string{"dn"}, + []string{"dn", "mail"}, nil, ) @@ -161,15 +160,15 @@ func (ldap *LdapService) GetUserDN(username string) (string, error) { searchResult, err := ldap.conn.Search(searchRequest) if err != nil { - return "", err + return "", "", err } if len(searchResult.Entries) != 1 { - return "", fmt.Errorf("multiple or no entries found for user %s", username) + return "", "", fmt.Errorf("multiple or no entries found for user %s", username) } - userDN := searchResult.Entries[0].DN - return userDN, nil + entry := searchResult.Entries[0] + return entry.DN, entry.GetAttributeValue("mail"), nil } func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) { From 5bcedf92fb327158e94afc4f28fdbe6e6c750f8a Mon Sep 17 00:00:00 2001 From: Stavros Date: Thu, 7 May 2026 16:45:42 +0300 Subject: [PATCH 2/2] chore: add ldap email logic back after main merge --- internal/controller/user_controller.go | 4 ++-- internal/middleware/context_middleware.go | 10 +++++++++- internal/model/users.go | 1 + internal/service/auth_service.go | 1 + 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 04e02a4c..6d0f3429 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -185,8 +185,8 @@ func (controller *UserController) loginHandler(c *gin.Context) { if search.Type == model.UserLDAP { sessionCookie.Provider = "ldap" - if userSearch.Email != "" { - sessionCookie.Email = userSearch.Email + if search.Email != "" { + sessionCookie.Email = search.Email } } diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 88e96462..c9e7f0b4 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -162,7 +162,11 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model userContext.LDAP.Groups = user.Groups userContext.LDAP.Name = utils.Capitalize(userContext.LDAP.Username) + userContext.LDAP.Email = utils.CompileUserEmail(userContext.LDAP.Username, m.config.CookieDomain) + if search.Email != "" { + userContext.LDAP.Email = search.Email + } case model.ProviderOAuth: _, exists := m.broker.GetService(userContext.OAuth.ID) @@ -240,11 +244,15 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model. BaseContext: model.BaseContext{ Username: username, Name: utils.Capitalize(username), - Email: utils.CompileUserEmail(username, m.config.CookieDomain), }, Groups: user.Groups, } userContext.Provider = model.ProviderLDAP + + userContext.LDAP.Email = utils.CompileUserEmail(username, m.config.CookieDomain) + if search.Email != "" { + userContext.LDAP.Email = search.Email + } } userContext.Authenticated = true diff --git a/internal/model/users.go b/internal/model/users.go index 48826fda..8dc9523d 100644 --- a/internal/model/users.go +++ b/internal/model/users.go @@ -21,5 +21,6 @@ type LocalUser struct { type UserSearch struct { Username string + Email string // used for LDAP, we can't throw it to LDAPUser because it would need another cache or an LDAP lookup every time Type UserSearchType } diff --git a/internal/service/auth_service.go b/internal/service/auth_service.go index f862eaac..77d445ef 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -137,6 +137,7 @@ func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) return &model.UserSearch{ Username: userDN, + Email: email, Type: model.UserLDAP, }, nil }