Compare commits

...

1 Commits

Author SHA1 Message Date
Ilyas a9eac7edd2 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 <noreply@anthropic.com>

* chore: add ldap email logic back after main merge

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-authored-by: Stavros <steveiliop56@gmail.com>
2026-05-11 15:40:15 +03:00
5 changed files with 22 additions and 9 deletions
+3
View File
@@ -189,6 +189,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
if search.Type == model.UserLDAP { if search.Type == model.UserLDAP {
sessionCookie.Provider = "ldap" sessionCookie.Provider = "ldap"
if search.Email != "" {
sessionCookie.Email = search.Email
}
} }
cookie, err := controller.auth.CreateSession(c, sessionCookie) cookie, err := controller.auth.CreateSession(c, sessionCookie)
+10 -1
View File
@@ -160,7 +160,12 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
userContext.LDAP.Groups = user.Groups userContext.LDAP.Groups = user.Groups
userContext.LDAP.Name = utils.Capitalize(userContext.LDAP.Username) userContext.LDAP.Name = utils.Capitalize(userContext.LDAP.Username)
userContext.LDAP.Email = utils.CompileUserEmail(userContext.LDAP.Username, m.runtime.CookieDomain) userContext.LDAP.Email = utils.CompileUserEmail(userContext.LDAP.Username, m.runtime.CookieDomain)
if search.Email != "" {
userContext.LDAP.Email = search.Email
}
case model.ProviderOAuth: case model.ProviderOAuth:
_, exists := m.broker.GetService(userContext.OAuth.ID) _, exists := m.broker.GetService(userContext.OAuth.ID)
@@ -238,11 +243,15 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model.
BaseContext: model.BaseContext{ BaseContext: model.BaseContext{
Username: username, Username: username,
Name: utils.Capitalize(username), Name: utils.Capitalize(username),
Email: utils.CompileUserEmail(username, m.runtime.CookieDomain),
}, },
Groups: user.Groups, Groups: user.Groups,
} }
userContext.Provider = model.ProviderLDAP userContext.Provider = model.ProviderLDAP
userContext.LDAP.Email = utils.CompileUserEmail(username, m.runtime.CookieDomain)
if search.Email != "" {
userContext.LDAP.Email = search.Email
}
} }
userContext.Authenticated = true userContext.Authenticated = true
+1
View File
@@ -21,5 +21,6 @@ type LocalUser struct {
type UserSearch struct { type UserSearch struct {
Username string 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 Type UserSearchType
} }
+2 -1
View File
@@ -130,7 +130,7 @@ func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error)
} }
if auth.ldap != nil { if auth.ldap != nil {
userDN, err := auth.ldap.GetUserDN(username) userDN, email, err := auth.ldap.GetUserInfo(username)
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to get ldap user: %w", err) 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{ return &model.UserSearch{
Username: userDN, Username: userDN,
Email: email,
Type: model.UserLDAP, Type: model.UserLDAP,
}, nil }, nil
} }
+6 -7
View File
@@ -134,8 +134,7 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
return ldap.conn, nil return ldap.conn, nil
} }
func (ldap *LdapService) GetUserDN(username string) (string, error) { func (ldap *LdapService) GetUserInfo(username string) (dn string, email string, err error) {
// Escape the username to prevent LDAP injection
escapedUsername := ldapgo.EscapeFilter(username) escapedUsername := ldapgo.EscapeFilter(username)
filter := fmt.Sprintf(ldap.config.LDAP.SearchFilter, escapedUsername) filter := fmt.Sprintf(ldap.config.LDAP.SearchFilter, escapedUsername)
@@ -143,7 +142,7 @@ func (ldap *LdapService) GetUserDN(username string) (string, error) {
ldap.config.LDAP.BaseDN, ldap.config.LDAP.BaseDN,
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false, ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
filter, filter,
[]string{"dn"}, []string{"dn", "mail"},
nil, nil,
) )
@@ -152,15 +151,15 @@ func (ldap *LdapService) GetUserDN(username string) (string, error) {
searchResult, err := ldap.conn.Search(searchRequest) searchResult, err := ldap.conn.Search(searchRequest)
if err != nil { if err != nil {
return "", err return "", "", err
} }
if len(searchResult.Entries) != 1 { 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 entry := searchResult.Entries[0]
return userDN, nil return entry.DN, entry.GetAttributeValue("mail"), nil
} }
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) { func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {