From a9eac7edd26e56b9b2c9b9bb25005cdbdd9889b0 Mon Sep 17 00:00:00 2001 From: Ilyas Date: Mon, 11 May 2026 14:40:15 +0200 Subject: [PATCH] fix(ldap): pass through LDAP mail attribute instead of crafting email (#834) * 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 * chore: add ldap email logic back after main merge --------- Co-authored-by: Claude Sonnet 4.6 Co-authored-by: Stavros --- internal/controller/user_controller.go | 3 +++ internal/middleware/context_middleware.go | 11 ++++++++++- internal/model/users.go | 1 + internal/service/auth_service.go | 3 ++- internal/service/ldap_service.go | 13 ++++++------- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/internal/controller/user_controller.go b/internal/controller/user_controller.go index 45a876bf..78e02803 100644 --- a/internal/controller/user_controller.go +++ b/internal/controller/user_controller.go @@ -189,6 +189,9 @@ func (controller *UserController) loginHandler(c *gin.Context) { if search.Type == model.UserLDAP { sessionCookie.Provider = "ldap" + if search.Email != "" { + sessionCookie.Email = search.Email + } } cookie, err := controller.auth.CreateSession(c, sessionCookie) diff --git a/internal/middleware/context_middleware.go b/internal/middleware/context_middleware.go index 6e6bbe56..e8b8cb00 100644 --- a/internal/middleware/context_middleware.go +++ b/internal/middleware/context_middleware.go @@ -160,7 +160,12 @@ 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.runtime.CookieDomain) + if search.Email != "" { + userContext.LDAP.Email = search.Email + } + case model.ProviderOAuth: _, exists := m.broker.GetService(userContext.OAuth.ID) @@ -238,11 +243,15 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model. BaseContext: model.BaseContext{ Username: username, Name: utils.Capitalize(username), - Email: utils.CompileUserEmail(username, m.runtime.CookieDomain), }, Groups: user.Groups, } userContext.Provider = model.ProviderLDAP + + userContext.LDAP.Email = utils.CompileUserEmail(username, m.runtime.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 a9139bb3..a721aa2b 100644 --- a/internal/service/auth_service.go +++ b/internal/service/auth_service.go @@ -130,7 +130,7 @@ func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) } if auth.ldap != nil { - userDN, err := auth.ldap.GetUserDN(username) + userDN, email, err := auth.ldap.GetUserInfo(username) if err != nil { return nil, fmt.Errorf("failed to get ldap user: %w", err) @@ -138,6 +138,7 @@ func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error) return &model.UserSearch{ Username: userDN, + Email: email, Type: model.UserLDAP, }, nil } diff --git a/internal/service/ldap_service.go b/internal/service/ldap_service.go index 9c031206..e9bbf1d9 100644 --- a/internal/service/ldap_service.go +++ b/internal/service/ldap_service.go @@ -134,8 +134,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.LDAP.SearchFilter, escapedUsername) @@ -143,7 +142,7 @@ func (ldap *LdapService) GetUserDN(username string) (string, error) { ldap.config.LDAP.BaseDN, ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false, filter, - []string{"dn"}, + []string{"dn", "mail"}, nil, ) @@ -152,15 +151,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) {