2023-02-15 08:14:59 +00:00
|
|
|
package ldap
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
|
|
|
"crypto/tls"
|
2024-06-28 13:46:54 +00:00
|
|
|
"encoding/base64"
|
2023-02-15 08:14:59 +00:00
|
|
|
"errors"
|
2023-03-24 15:18:56 +00:00
|
|
|
"net"
|
|
|
|
"net/url"
|
2023-02-15 08:14:59 +00:00
|
|
|
"strconv"
|
2023-03-24 15:18:56 +00:00
|
|
|
"time"
|
2024-06-28 13:46:54 +00:00
|
|
|
"unicode/utf8"
|
2023-02-15 08:14:59 +00:00
|
|
|
|
|
|
|
"github.com/go-ldap/ldap/v3"
|
2024-06-25 19:04:10 +00:00
|
|
|
"github.com/zitadel/logging"
|
2023-02-15 08:14:59 +00:00
|
|
|
"golang.org/x/text/language"
|
|
|
|
|
2023-03-14 19:20:38 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/domain"
|
2023-02-15 08:14:59 +00:00
|
|
|
"github.com/zitadel/zitadel/internal/idp"
|
|
|
|
)
|
|
|
|
|
|
|
|
var ErrNoSingleUser = errors.New("user does not exist or too many entries returned")
|
2023-03-24 15:18:56 +00:00
|
|
|
var ErrFailedLogin = errors.New("user failed to login")
|
2023-02-15 08:14:59 +00:00
|
|
|
|
|
|
|
var _ idp.Session = (*Session)(nil)
|
|
|
|
|
|
|
|
type Session struct {
|
|
|
|
Provider *Provider
|
|
|
|
loginUrl string
|
2023-03-24 15:18:56 +00:00
|
|
|
User string
|
|
|
|
Password string
|
2023-08-16 11:29:57 +00:00
|
|
|
Entry *ldap.Entry
|
2023-02-15 08:14:59 +00:00
|
|
|
}
|
|
|
|
|
2023-09-29 09:26:14 +00:00
|
|
|
// GetAuth implements the [idp.Session] interface.
|
|
|
|
func (s *Session) GetAuth(ctx context.Context) (string, bool) {
|
|
|
|
return idp.Redirect(s.loginUrl)
|
2023-02-15 08:14:59 +00:00
|
|
|
}
|
2023-03-24 15:18:56 +00:00
|
|
|
|
|
|
|
func (s *Session) FetchUser(_ context.Context) (_ idp.User, err error) {
|
|
|
|
var user *ldap.Entry
|
|
|
|
for _, server := range s.Provider.servers {
|
|
|
|
user, err = tryBind(server,
|
|
|
|
s.Provider.startTLS,
|
|
|
|
s.Provider.bindDN,
|
|
|
|
s.Provider.bindPassword,
|
|
|
|
s.Provider.baseDN,
|
|
|
|
s.Provider.getNecessaryAttributes(),
|
|
|
|
s.Provider.userObjectClasses,
|
|
|
|
s.Provider.userFilters,
|
|
|
|
s.User,
|
|
|
|
s.Password, s.Provider.timeout)
|
|
|
|
// If there were invalid credentials or multiple users with the credentials cancel process
|
|
|
|
if err != nil && (errors.Is(err, ErrFailedLogin) || errors.Is(err, ErrNoSingleUser)) {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
// If a user bind was successful and user is filled continue with login, otherwise try next server
|
|
|
|
if err == nil && user != nil {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
2023-02-15 08:14:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-08-16 11:29:57 +00:00
|
|
|
s.Entry = user
|
2023-02-15 08:14:59 +00:00
|
|
|
|
2023-03-24 15:18:56 +00:00
|
|
|
return mapLDAPEntryToUser(
|
|
|
|
user,
|
|
|
|
s.Provider.idAttribute,
|
|
|
|
s.Provider.firstNameAttribute,
|
|
|
|
s.Provider.lastNameAttribute,
|
|
|
|
s.Provider.displayNameAttribute,
|
|
|
|
s.Provider.nickNameAttribute,
|
|
|
|
s.Provider.preferredUsernameAttribute,
|
|
|
|
s.Provider.emailAttribute,
|
|
|
|
s.Provider.emailVerifiedAttribute,
|
|
|
|
s.Provider.phoneAttribute,
|
|
|
|
s.Provider.phoneVerifiedAttribute,
|
|
|
|
s.Provider.preferredLanguageAttribute,
|
|
|
|
s.Provider.avatarURLAttribute,
|
|
|
|
s.Provider.profileAttribute,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func tryBind(
|
|
|
|
server string,
|
|
|
|
startTLS bool,
|
|
|
|
bindDN string,
|
|
|
|
bindPassword string,
|
|
|
|
baseDN string,
|
|
|
|
attributes []string,
|
|
|
|
objectClasses []string,
|
|
|
|
userFilters []string,
|
|
|
|
username string,
|
|
|
|
password string,
|
|
|
|
timeout time.Duration,
|
|
|
|
) (*ldap.Entry, error) {
|
|
|
|
conn, err := getConnection(server, startTLS, timeout)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
if err := conn.Bind(bindDN, bindPassword); err != nil {
|
|
|
|
return nil, err
|
2023-02-15 08:14:59 +00:00
|
|
|
}
|
|
|
|
|
2023-03-24 15:18:56 +00:00
|
|
|
return trySearchAndUserBind(
|
|
|
|
conn,
|
|
|
|
baseDN,
|
|
|
|
attributes,
|
|
|
|
objectClasses,
|
|
|
|
userFilters,
|
|
|
|
username,
|
|
|
|
password,
|
|
|
|
timeout,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
func getConnection(
|
|
|
|
server string,
|
|
|
|
startTLS bool,
|
|
|
|
timeout time.Duration,
|
|
|
|
) (*ldap.Conn, error) {
|
|
|
|
if timeout == 0 {
|
|
|
|
timeout = ldap.DefaultTimeout
|
|
|
|
}
|
|
|
|
|
|
|
|
conn, err := ldap.DialURL(server, ldap.DialWithDialer(&net.Dialer{Timeout: timeout}))
|
2023-02-15 08:14:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
2023-03-24 15:18:56 +00:00
|
|
|
u, err := url.Parse(server)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if u.Scheme == "ldaps" && startTLS {
|
|
|
|
err = conn.StartTLS(&tls.Config{ServerName: u.Host})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return conn, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func trySearchAndUserBind(
|
|
|
|
conn *ldap.Conn,
|
|
|
|
baseDN string,
|
|
|
|
attributes []string,
|
|
|
|
objectClasses []string,
|
|
|
|
userFilters []string,
|
|
|
|
username string,
|
|
|
|
password string,
|
|
|
|
timeout time.Duration,
|
|
|
|
) (*ldap.Entry, error) {
|
|
|
|
searchQuery := queriesAndToSearchQuery(
|
|
|
|
objectClassesToSearchQuery(objectClasses),
|
|
|
|
queriesOrToSearchQuery(
|
|
|
|
userFiltersToSearchQuery(userFilters, username),
|
|
|
|
),
|
|
|
|
)
|
|
|
|
|
2023-02-15 08:14:59 +00:00
|
|
|
// Search for user with the unique attribute for the userDN
|
|
|
|
searchRequest := ldap.NewSearchRequest(
|
2023-03-24 15:18:56 +00:00
|
|
|
baseDN,
|
|
|
|
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, int(timeout.Seconds()), false,
|
|
|
|
searchQuery,
|
|
|
|
attributes,
|
2023-02-15 08:14:59 +00:00
|
|
|
nil,
|
|
|
|
)
|
|
|
|
|
2023-03-24 15:18:56 +00:00
|
|
|
sr, err := conn.Search(searchRequest)
|
2023-02-15 08:14:59 +00:00
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
if len(sr.Entries) != 1 {
|
2024-06-25 19:04:10 +00:00
|
|
|
logging.WithFields("entries", len(sr.Entries)).Info("ldap: no single user found")
|
2023-02-15 08:14:59 +00:00
|
|
|
return nil, ErrNoSingleUser
|
|
|
|
}
|
|
|
|
|
|
|
|
user := sr.Entries[0]
|
|
|
|
// Bind as the user to verify their password
|
2023-03-24 15:18:56 +00:00
|
|
|
if err = conn.Bind(user.DN, password); err != nil {
|
2024-06-25 19:04:10 +00:00
|
|
|
logging.WithFields("userDN", user.DN).WithError(err).Info("ldap user bind failed")
|
2023-03-24 15:18:56 +00:00
|
|
|
return nil, ErrFailedLogin
|
2023-02-15 08:14:59 +00:00
|
|
|
}
|
2023-03-24 15:18:56 +00:00
|
|
|
return user, nil
|
|
|
|
}
|
2023-02-15 08:14:59 +00:00
|
|
|
|
2023-03-24 15:18:56 +00:00
|
|
|
func queriesAndToSearchQuery(queries ...string) string {
|
|
|
|
if len(queries) == 0 {
|
|
|
|
return ""
|
2023-02-15 08:14:59 +00:00
|
|
|
}
|
2023-03-24 15:18:56 +00:00
|
|
|
if len(queries) == 1 {
|
|
|
|
return queries[0]
|
|
|
|
}
|
|
|
|
joinQueries := "(&"
|
|
|
|
for _, s := range queries {
|
|
|
|
joinQueries += s
|
|
|
|
}
|
|
|
|
return joinQueries + ")"
|
|
|
|
}
|
|
|
|
|
|
|
|
func queriesOrToSearchQuery(queries ...string) string {
|
|
|
|
if len(queries) == 0 {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
if len(queries) == 1 {
|
|
|
|
return queries[0]
|
|
|
|
}
|
|
|
|
joinQueries := "(|"
|
|
|
|
for _, s := range queries {
|
|
|
|
joinQueries += s
|
|
|
|
}
|
|
|
|
return joinQueries + ")"
|
|
|
|
}
|
|
|
|
|
|
|
|
func objectClassesToSearchQuery(classes []string) string {
|
|
|
|
searchQuery := ""
|
|
|
|
for _, class := range classes {
|
|
|
|
searchQuery += "(objectClass=" + class + ")"
|
|
|
|
}
|
|
|
|
return searchQuery
|
|
|
|
}
|
|
|
|
|
|
|
|
func userFiltersToSearchQuery(filters []string, username string) string {
|
|
|
|
searchQuery := ""
|
|
|
|
for _, filter := range filters {
|
|
|
|
searchQuery += "(" + filter + "=" + ldap.EscapeFilter(username) + ")"
|
|
|
|
}
|
|
|
|
return searchQuery
|
|
|
|
}
|
|
|
|
|
|
|
|
func mapLDAPEntryToUser(
|
|
|
|
user *ldap.Entry,
|
|
|
|
idAttribute,
|
|
|
|
firstNameAttribute,
|
|
|
|
lastNameAttribute,
|
|
|
|
displayNameAttribute,
|
|
|
|
nickNameAttribute,
|
|
|
|
preferredUsernameAttribute,
|
|
|
|
emailAttribute,
|
|
|
|
emailVerifiedAttribute,
|
|
|
|
phoneAttribute,
|
|
|
|
phoneVerifiedAttribute,
|
|
|
|
preferredLanguageAttribute,
|
|
|
|
avatarURLAttribute,
|
|
|
|
profileAttribute string,
|
|
|
|
) (_ *User, err error) {
|
|
|
|
var emailVerified bool
|
|
|
|
if v := user.GetAttributeValue(emailVerifiedAttribute); v != "" {
|
|
|
|
emailVerified, err = strconv.ParseBool(v)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
}
|
|
|
|
var phoneVerified bool
|
|
|
|
if v := user.GetAttributeValue(phoneVerifiedAttribute); v != "" {
|
|
|
|
phoneVerified, err = strconv.ParseBool(v)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
2023-02-15 08:14:59 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return NewUser(
|
2024-06-28 13:46:54 +00:00
|
|
|
getAttributeValue(user, idAttribute),
|
|
|
|
getAttributeValue(user, firstNameAttribute),
|
|
|
|
getAttributeValue(user, lastNameAttribute),
|
|
|
|
getAttributeValue(user, displayNameAttribute),
|
|
|
|
getAttributeValue(user, nickNameAttribute),
|
|
|
|
getAttributeValue(user, preferredUsernameAttribute),
|
2023-03-24 15:18:56 +00:00
|
|
|
domain.EmailAddress(user.GetAttributeValue(emailAttribute)),
|
2023-02-15 08:14:59 +00:00
|
|
|
emailVerified,
|
2023-03-24 15:18:56 +00:00
|
|
|
domain.PhoneNumber(user.GetAttributeValue(phoneAttribute)),
|
2023-02-15 08:14:59 +00:00
|
|
|
phoneVerified,
|
2023-03-24 15:18:56 +00:00
|
|
|
language.Make(user.GetAttributeValue(preferredLanguageAttribute)),
|
|
|
|
user.GetAttributeValue(avatarURLAttribute),
|
|
|
|
user.GetAttributeValue(profileAttribute),
|
2023-02-15 08:14:59 +00:00
|
|
|
), nil
|
|
|
|
}
|
2024-06-28 13:46:54 +00:00
|
|
|
|
|
|
|
func getAttributeValue(user *ldap.Entry, attribute string) string {
|
|
|
|
// return an empty string if no attribute is needed
|
|
|
|
if attribute == "" {
|
|
|
|
return ""
|
|
|
|
}
|
|
|
|
value := user.GetAttributeValue(attribute)
|
|
|
|
if utf8.ValidString(value) {
|
|
|
|
return value
|
|
|
|
}
|
|
|
|
return base64.StdEncoding.EncodeToString(user.GetRawAttributeValue(attribute))
|
|
|
|
}
|