2019-04-24 21:52:08 +00:00
|
|
|
package authentication
|
|
|
|
|
|
|
|
import (
|
2019-12-06 08:15:54 +00:00
|
|
|
"crypto/tls"
|
2019-04-24 21:52:08 +00:00
|
|
|
"fmt"
|
2019-12-06 08:15:54 +00:00
|
|
|
"net/url"
|
2019-04-24 21:52:08 +00:00
|
|
|
"strings"
|
|
|
|
|
2019-12-24 02:14:52 +00:00
|
|
|
"github.com/authelia/authelia/internal/configuration/schema"
|
|
|
|
"github.com/authelia/authelia/internal/logging"
|
2019-04-24 21:52:08 +00:00
|
|
|
"gopkg.in/ldap.v3"
|
|
|
|
)
|
|
|
|
|
|
|
|
// LDAPUserProvider is a provider using a LDAP or AD as a user database.
|
|
|
|
type LDAPUserProvider struct {
|
|
|
|
configuration schema.LDAPAuthenticationBackendConfiguration
|
2019-12-06 08:15:54 +00:00
|
|
|
|
|
|
|
connectionFactory LDAPConnectionFactory
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
|
|
|
|
2019-12-06 08:15:54 +00:00
|
|
|
// NewLDAPUserProvider creates a new instance of LDAPUserProvider.
|
|
|
|
func NewLDAPUserProvider(configuration schema.LDAPAuthenticationBackendConfiguration) *LDAPUserProvider {
|
|
|
|
return &LDAPUserProvider{
|
|
|
|
configuration: configuration,
|
|
|
|
connectionFactory: NewLDAPConnectionFactoryImpl(),
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
2019-12-06 08:15:54 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
func NewLDAPUserProviderWithFactory(configuration schema.LDAPAuthenticationBackendConfiguration,
|
|
|
|
connectionFactory LDAPConnectionFactory) *LDAPUserProvider {
|
|
|
|
return &LDAPUserProvider{
|
|
|
|
configuration: configuration,
|
|
|
|
connectionFactory: connectionFactory,
|
|
|
|
}
|
|
|
|
}
|
2019-04-24 21:52:08 +00:00
|
|
|
|
2019-12-06 08:15:54 +00:00
|
|
|
func (p *LDAPUserProvider) connect(userDN string, password string) (LDAPConnection, error) {
|
|
|
|
var newConnection LDAPConnection
|
|
|
|
|
|
|
|
url, err := url.Parse(p.configuration.URL)
|
2019-04-24 21:52:08 +00:00
|
|
|
|
|
|
|
if err != nil {
|
2019-12-06 08:15:54 +00:00
|
|
|
return nil, fmt.Errorf("Unable to parse URL to LDAP: %s", url)
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
|
|
|
|
2019-12-06 08:15:54 +00:00
|
|
|
if url.Scheme == "ldaps" {
|
|
|
|
logging.Logger().Debug("LDAP client starts a TLS session")
|
|
|
|
conn, err := p.connectionFactory.DialTLS("tcp", url.Host, &tls.Config{
|
|
|
|
InsecureSkipVerify: p.configuration.SkipVerify,
|
|
|
|
})
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
newConnection = conn
|
|
|
|
} else {
|
|
|
|
logging.Logger().Debug("LDAP client starts a session over raw TCP")
|
|
|
|
conn, err := p.connectionFactory.Dial("tcp", url.Host)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
newConnection = conn
|
|
|
|
}
|
|
|
|
|
|
|
|
if err := newConnection.Bind(userDN, password); err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
return newConnection, nil
|
2019-04-24 21:52:08 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// CheckUserPassword checks if provided password matches for the given user.
|
|
|
|
func (p *LDAPUserProvider) CheckUserPassword(username string, password string) (bool, error) {
|
|
|
|
adminClient, err := p.connect(p.configuration.User, p.configuration.Password)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
defer adminClient.Close()
|
|
|
|
|
|
|
|
userDN, err := p.getUserDN(adminClient, username)
|
|
|
|
if err != nil {
|
|
|
|
return false, err
|
|
|
|
}
|
|
|
|
|
|
|
|
conn, err := p.connect(userDN, password)
|
|
|
|
if err != nil {
|
|
|
|
return false, fmt.Errorf("Authentication of user %s failed. Cause: %s", username, err)
|
|
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
|
2020-01-20 19:34:53 +00:00
|
|
|
// OWASP recommends to escape some special characters
|
|
|
|
// https://github.com/OWASP/CheatSheetSeries/blob/master/cheatsheets/LDAP_Injection_Prevention_Cheat_Sheet.md
|
|
|
|
const SpecialLDAPRunes = "\\,#+<>;\"="
|
|
|
|
|
|
|
|
func (p *LDAPUserProvider) ldapEscape(input string) string {
|
|
|
|
for _, c := range SpecialLDAPRunes {
|
|
|
|
input = strings.ReplaceAll(input, string(c), fmt.Sprintf("\\%c", c))
|
|
|
|
}
|
|
|
|
return input
|
|
|
|
}
|
|
|
|
|
2019-12-06 08:15:54 +00:00
|
|
|
func (p *LDAPUserProvider) getUserAttribute(conn LDAPConnection, username string, attribute string) ([]string, error) {
|
2019-04-24 21:52:08 +00:00
|
|
|
client, err := p.connect(p.configuration.User, p.configuration.Password)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer client.Close()
|
|
|
|
|
2020-01-20 19:34:53 +00:00
|
|
|
username = p.ldapEscape(username)
|
2019-04-24 21:52:08 +00:00
|
|
|
userFilter := strings.Replace(p.configuration.UsersFilter, "{0}", username, -1)
|
2019-12-27 11:07:53 +00:00
|
|
|
baseDN := p.configuration.BaseDN
|
2019-12-19 03:21:55 +00:00
|
|
|
if p.configuration.AdditionalUsersDN != "" {
|
|
|
|
baseDN = p.configuration.AdditionalUsersDN + "," + baseDN
|
|
|
|
}
|
2019-04-24 21:52:08 +00:00
|
|
|
|
|
|
|
// Search for the given username
|
|
|
|
searchRequest := ldap.NewSearchRequest(
|
|
|
|
baseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
|
|
|
1, 0, false, userFilter, []string{attribute}, nil,
|
|
|
|
)
|
|
|
|
|
|
|
|
sr, err := client.Search(searchRequest)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Cannot find user DN of user %s. Cause: %s", username, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(sr.Entries) != 1 {
|
|
|
|
return nil, fmt.Errorf("No %s found for user %s", attribute, username)
|
|
|
|
}
|
|
|
|
|
|
|
|
if attribute == "dn" {
|
|
|
|
return []string{sr.Entries[0].DN}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
return sr.Entries[0].Attributes[0].Values, nil
|
|
|
|
}
|
|
|
|
|
2019-12-06 08:15:54 +00:00
|
|
|
func (p *LDAPUserProvider) getUserDN(conn LDAPConnection, username string) (string, error) {
|
2019-04-24 21:52:08 +00:00
|
|
|
values, err := p.getUserAttribute(conn, username, "dn")
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(values) != 1 {
|
|
|
|
return "", fmt.Errorf("DN attribute of user %s must be set", username)
|
|
|
|
}
|
|
|
|
|
|
|
|
return values[0], nil
|
|
|
|
}
|
|
|
|
|
2019-12-06 08:15:54 +00:00
|
|
|
func (p *LDAPUserProvider) getUserUID(conn LDAPConnection, username string) (string, error) {
|
2019-04-24 21:52:08 +00:00
|
|
|
values, err := p.getUserAttribute(conn, username, "uid")
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
|
|
|
|
if len(values) != 1 {
|
|
|
|
return "", fmt.Errorf("UID attribute of user %s must be set", username)
|
|
|
|
}
|
|
|
|
|
|
|
|
return values[0], nil
|
|
|
|
}
|
|
|
|
|
2019-12-06 08:15:54 +00:00
|
|
|
func (p *LDAPUserProvider) createGroupsFilter(conn LDAPConnection, username string) (string, error) {
|
2019-12-27 16:55:00 +00:00
|
|
|
if strings.Contains(p.configuration.GroupsFilter, "{0}") {
|
2019-04-24 21:52:08 +00:00
|
|
|
return strings.Replace(p.configuration.GroupsFilter, "{0}", username, -1), nil
|
2019-12-27 16:55:00 +00:00
|
|
|
} else if strings.Contains(p.configuration.GroupsFilter, "{dn}") {
|
2019-04-24 21:52:08 +00:00
|
|
|
userDN, err := p.getUserDN(conn, username)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return strings.Replace(p.configuration.GroupsFilter, "{dn}", userDN, -1), nil
|
2019-12-27 16:55:00 +00:00
|
|
|
} else if strings.Contains(p.configuration.GroupsFilter, "{uid}") {
|
2019-04-24 21:52:08 +00:00
|
|
|
userUID, err := p.getUserUID(conn, username)
|
|
|
|
if err != nil {
|
|
|
|
return "", err
|
|
|
|
}
|
|
|
|
return strings.Replace(p.configuration.GroupsFilter, "{uid}", userUID, -1), nil
|
|
|
|
}
|
|
|
|
return p.configuration.GroupsFilter, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// GetDetails retrieve the groups a user belongs to.
|
|
|
|
func (p *LDAPUserProvider) GetDetails(username string) (*UserDetails, error) {
|
|
|
|
conn, err := p.connect(p.configuration.User, p.configuration.Password)
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
defer conn.Close()
|
|
|
|
|
|
|
|
groupsFilter, err := p.createGroupsFilter(conn, username)
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Unable to create group filter for user %s. Cause: %s", username, err)
|
|
|
|
}
|
|
|
|
|
2019-12-27 11:07:53 +00:00
|
|
|
groupBaseDN := p.configuration.BaseDN
|
2019-12-19 03:21:55 +00:00
|
|
|
if p.configuration.AdditionalGroupsDN != "" {
|
|
|
|
groupBaseDN = p.configuration.AdditionalGroupsDN + "," + groupBaseDN
|
|
|
|
}
|
2019-04-24 21:52:08 +00:00
|
|
|
|
|
|
|
// Search for the given username
|
|
|
|
searchGroupRequest := ldap.NewSearchRequest(
|
|
|
|
groupBaseDN, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases,
|
|
|
|
0, 0, false, groupsFilter, []string{p.configuration.GroupNameAttribute}, nil,
|
|
|
|
)
|
|
|
|
|
|
|
|
sr, err := conn.Search(searchGroupRequest)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Unable to retrieve groups of user %s. Cause: %s", username, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
groups := make([]string, 0)
|
|
|
|
|
|
|
|
for _, res := range sr.Entries {
|
|
|
|
// append all values of the document. Normally there should be only one per document.
|
|
|
|
groups = append(groups, res.Attributes[0].Values...)
|
|
|
|
}
|
|
|
|
|
|
|
|
userDN, err := p.getUserDN(conn, username)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
searchEmailRequest := ldap.NewSearchRequest(
|
|
|
|
userDN, ldap.ScopeBaseObject, ldap.NeverDerefAliases,
|
|
|
|
0, 0, false, "(cn=*)", []string{p.configuration.MailAttribute}, nil,
|
|
|
|
)
|
|
|
|
|
|
|
|
sr, err = conn.Search(searchEmailRequest)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return nil, fmt.Errorf("Unable to retrieve email of user %s. Cause: %s", username, err)
|
|
|
|
}
|
|
|
|
|
|
|
|
emails := make([]string, 0)
|
|
|
|
|
|
|
|
for _, res := range sr.Entries {
|
|
|
|
// append all values of the document. Normally there should be only one per document.
|
|
|
|
emails = append(emails, res.Attributes[0].Values...)
|
|
|
|
}
|
|
|
|
|
|
|
|
return &UserDetails{
|
|
|
|
Emails: emails,
|
|
|
|
Groups: groups,
|
|
|
|
}, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// UpdatePassword update the password of the given user.
|
|
|
|
func (p *LDAPUserProvider) UpdatePassword(username string, newPassword string) error {
|
|
|
|
client, err := p.connect(p.configuration.User, p.configuration.Password)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Unable to update password. Cause: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
userDN, err := p.getUserDN(client, username)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Unable to update password. Cause: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
modifyRequest := ldap.NewModifyRequest(userDN, nil)
|
|
|
|
|
|
|
|
modifyRequest.Replace("userPassword", []string{newPassword})
|
|
|
|
|
|
|
|
err = client.Modify(modifyRequest)
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
return fmt.Errorf("Unable to update password. Cause: %s", err)
|
|
|
|
}
|
|
|
|
|
|
|
|
return nil
|
|
|
|
}
|