Skip to content

Commit a9eac7e

Browse files
itasliclaudesteveiliop56
authored
fix(ldap): pass through LDAP mail attribute instead of crafting email (tinyauthapp#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>
1 parent a635179 commit a9eac7e

5 files changed

Lines changed: 22 additions & 9 deletions

File tree

internal/controller/user_controller.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,9 @@ func (controller *UserController) loginHandler(c *gin.Context) {
189189

190190
if search.Type == model.UserLDAP {
191191
sessionCookie.Provider = "ldap"
192+
if search.Email != "" {
193+
sessionCookie.Email = search.Email
194+
}
192195
}
193196

194197
cookie, err := controller.auth.CreateSession(c, sessionCookie)

internal/middleware/context_middleware.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,7 +160,12 @@ func (m *ContextMiddleware) cookieAuth(ctx context.Context, uuid string) (*model
160160

161161
userContext.LDAP.Groups = user.Groups
162162
userContext.LDAP.Name = utils.Capitalize(userContext.LDAP.Username)
163+
163164
userContext.LDAP.Email = utils.CompileUserEmail(userContext.LDAP.Username, m.runtime.CookieDomain)
165+
if search.Email != "" {
166+
userContext.LDAP.Email = search.Email
167+
}
168+
164169
case model.ProviderOAuth:
165170
_, exists := m.broker.GetService(userContext.OAuth.ID)
166171

@@ -238,11 +243,15 @@ func (m *ContextMiddleware) basicAuth(username string, password string) (*model.
238243
BaseContext: model.BaseContext{
239244
Username: username,
240245
Name: utils.Capitalize(username),
241-
Email: utils.CompileUserEmail(username, m.runtime.CookieDomain),
242246
},
243247
Groups: user.Groups,
244248
}
245249
userContext.Provider = model.ProviderLDAP
250+
251+
userContext.LDAP.Email = utils.CompileUserEmail(username, m.runtime.CookieDomain)
252+
if search.Email != "" {
253+
userContext.LDAP.Email = search.Email
254+
}
246255
}
247256

248257
userContext.Authenticated = true

internal/model/users.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,5 +21,6 @@ type LocalUser struct {
2121

2222
type UserSearch struct {
2323
Username string
24+
Email string // used for LDAP, we can't throw it to LDAPUser because it would need another cache or an LDAP lookup every time
2425
Type UserSearchType
2526
}

internal/service/auth_service.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -130,14 +130,15 @@ func (auth *AuthService) SearchUser(username string) (*model.UserSearch, error)
130130
}
131131

132132
if auth.ldap != nil {
133-
userDN, err := auth.ldap.GetUserDN(username)
133+
userDN, email, err := auth.ldap.GetUserInfo(username)
134134

135135
if err != nil {
136136
return nil, fmt.Errorf("failed to get ldap user: %w", err)
137137
}
138138

139139
return &model.UserSearch{
140140
Username: userDN,
141+
Email: email,
141142
Type: model.UserLDAP,
142143
}, nil
143144
}

internal/service/ldap_service.go

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -134,16 +134,15 @@ func (ldap *LdapService) connect() (*ldapgo.Conn, error) {
134134
return ldap.conn, nil
135135
}
136136

137-
func (ldap *LdapService) GetUserDN(username string) (string, error) {
138-
// Escape the username to prevent LDAP injection
137+
func (ldap *LdapService) GetUserInfo(username string) (dn string, email string, err error) {
139138
escapedUsername := ldapgo.EscapeFilter(username)
140139
filter := fmt.Sprintf(ldap.config.LDAP.SearchFilter, escapedUsername)
141140

142141
searchRequest := ldapgo.NewSearchRequest(
143142
ldap.config.LDAP.BaseDN,
144143
ldapgo.ScopeWholeSubtree, ldapgo.NeverDerefAliases, 0, 0, false,
145144
filter,
146-
[]string{"dn"},
145+
[]string{"dn", "mail"},
147146
nil,
148147
)
149148

@@ -152,15 +151,15 @@ func (ldap *LdapService) GetUserDN(username string) (string, error) {
152151

153152
searchResult, err := ldap.conn.Search(searchRequest)
154153
if err != nil {
155-
return "", err
154+
return "", "", err
156155
}
157156

158157
if len(searchResult.Entries) != 1 {
159-
return "", fmt.Errorf("multiple or no entries found for user %s", username)
158+
return "", "", fmt.Errorf("multiple or no entries found for user %s", username)
160159
}
161160

162-
userDN := searchResult.Entries[0].DN
163-
return userDN, nil
161+
entry := searchResult.Entries[0]
162+
return entry.DN, entry.GetAttributeValue("mail"), nil
164163
}
165164

166165
func (ldap *LdapService) GetUserGroups(userDN string) ([]string, error) {

0 commit comments

Comments
 (0)