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"
2020-04-05 12:37:21 +00:00
"github.com/go-ldap/ldap/v3"
2020-11-27 09:59:22 +00:00
"golang.org/x/text/encoding/unicode"
2020-04-05 12:37:21 +00:00
2019-12-24 02:14:52 +00:00
"github.com/authelia/authelia/internal/configuration/schema"
"github.com/authelia/authelia/internal/logging"
2020-12-03 05:23:52 +00:00
"github.com/authelia/authelia/internal/utils"
2019-04-24 21:52:08 +00:00
)
// LDAPUserProvider is a provider using a LDAP or AD as a user database.
type LDAPUserProvider struct {
configuration schema . LDAPAuthenticationBackendConfiguration
2020-12-03 05:23:52 +00:00
tlsConfig * tls . Config
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 {
2020-12-03 05:23:52 +00:00
minimumTLSVersion , _ := utils . TLSStringToTLSConfigVersion ( configuration . MinimumTLSVersion )
// TODO: RELEASE-4.27.0 Deprecated Completely in this release.
logger := logging . Logger ( )
if strings . Contains ( configuration . UsersFilter , "{0}" ) {
logger . Warnf ( "DEPRECATION NOTICE: LDAP Users Filter will no longer support replacing `{0}` in 4.27.0. Please use `{input}` instead." )
configuration . UsersFilter = strings . ReplaceAll ( configuration . UsersFilter , "{0}" , "{input}" )
}
if strings . Contains ( configuration . GroupsFilter , "{0}" ) {
logger . Warnf ( "DEPRECATION NOTICE: LDAP Groups Filter will no longer support replacing `{0}` in 4.27.0. Please use `{input}` instead." )
configuration . GroupsFilter = strings . ReplaceAll ( configuration . GroupsFilter , "{0}" , "{input}" )
}
if strings . Contains ( configuration . GroupsFilter , "{1}" ) {
logger . Warnf ( "DEPRECATION NOTICE: LDAP Groups Filter will no longer support replacing `{1}` in 4.27.0. Please use `{username}` instead." )
configuration . GroupsFilter = strings . ReplaceAll ( configuration . GroupsFilter , "{1}" , "{username}" )
}
// TODO: RELEASE-4.27.0 Deprecated Completely in this release.
configuration . UsersFilter = strings . ReplaceAll ( configuration . UsersFilter , "{username_attribute}" , configuration . UsernameAttribute )
configuration . UsersFilter = strings . ReplaceAll ( configuration . UsersFilter , "{mail_attribute}" , configuration . MailAttribute )
configuration . UsersFilter = strings . ReplaceAll ( configuration . UsersFilter , "{display_name_attribute}" , configuration . DisplayNameAttribute )
2019-12-06 08:15:54 +00:00
return & LDAPUserProvider {
2020-12-03 05:23:52 +00:00
configuration : configuration ,
tlsConfig : & tls . Config { InsecureSkipVerify : configuration . SkipVerify , MinVersion : minimumTLSVersion } , //nolint:gosec // Disabling InsecureSkipVerify is an informed choice by users.
2019-12-06 08:15:54 +00:00
connectionFactory : NewLDAPConnectionFactoryImpl ( ) ,
2019-04-24 21:52:08 +00:00
}
2019-12-06 08:15:54 +00:00
}
2020-04-20 21:03:38 +00:00
// NewLDAPUserProviderWithFactory creates a new instance of LDAPUserProvider with existing factory.
2020-12-03 05:23:52 +00:00
func NewLDAPUserProviderWithFactory ( configuration schema . LDAPAuthenticationBackendConfiguration , connectionFactory LDAPConnectionFactory ) * LDAPUserProvider {
provider := NewLDAPUserProvider ( configuration )
provider . connectionFactory = connectionFactory
return provider
2019-12-06 08:15:54 +00:00
}
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
2020-12-03 05:23:52 +00:00
ldapURL , err := url . Parse ( p . configuration . URL )
2019-04-24 21:52:08 +00:00
if err != nil {
2020-12-03 05:23:52 +00:00
return nil , fmt . Errorf ( "Unable to parse URL to LDAP: %s" , ldapURL )
2019-04-24 21:52:08 +00:00
}
2020-12-03 05:23:52 +00:00
if ldapURL . Scheme == "ldaps" {
2020-02-27 22:21:07 +00:00
logging . Logger ( ) . Trace ( "LDAP client starts a TLS session" )
2020-05-05 19:35:32 +00:00
2020-12-03 05:23:52 +00:00
conn , err := p . connectionFactory . DialTLS ( "tcp" , ldapURL . Host , p . tlsConfig )
2019-12-06 08:15:54 +00:00
if err != nil {
return nil , err
}
2020-05-05 19:35:32 +00:00
2019-12-06 08:15:54 +00:00
newConnection = conn
} else {
2020-02-27 22:21:07 +00:00
logging . Logger ( ) . Trace ( "LDAP client starts a session over raw TCP" )
2020-12-03 05:23:52 +00:00
conn , err := p . connectionFactory . Dial ( "tcp" , ldapURL . Host )
2019-12-06 08:15:54 +00:00
if err != nil {
return nil , err
}
newConnection = conn
}
2020-12-03 05:23:52 +00:00
if p . configuration . StartTLS {
if err := newConnection . StartTLS ( p . tlsConfig ) ; err != nil {
return nil , err
}
}
2019-12-06 08:15:54 +00:00
if err := newConnection . Bind ( userDN , password ) ; err != nil {
return nil , err
}
2020-05-05 19:35:32 +00:00
2019-12-06 08:15:54 +00:00
return newConnection , nil
2019-04-24 21:52:08 +00:00
}
// CheckUserPassword checks if provided password matches for the given user.
2020-03-30 22:36:04 +00:00
func ( p * LDAPUserProvider ) CheckUserPassword ( inputUsername string , password string ) ( bool , error ) {
2019-04-24 21:52:08 +00:00
adminClient , err := p . connect ( p . configuration . User , p . configuration . Password )
if err != nil {
return false , err
}
defer adminClient . Close ( )
2020-03-30 22:36:04 +00:00
profile , err := p . getUserProfile ( adminClient , inputUsername )
2019-04-24 21:52:08 +00:00
if err != nil {
return false , err
}
2020-03-15 07:10:25 +00:00
conn , err := p . connect ( profile . DN , password )
2019-04-24 21:52:08 +00:00
if err != nil {
2020-03-30 22:36:04 +00:00
return false , fmt . Errorf ( "Authentication of user %s failed. Cause: %s" , inputUsername , err )
2019-04-24 21:52:08 +00:00
}
defer conn . Close ( )
return true , nil
}
2020-03-30 22:36:04 +00:00
func ( p * LDAPUserProvider ) ldapEscape ( inputUsername string ) string {
inputUsername = ldap . EscapeFilter ( inputUsername )
for _ , c := range specialLDAPRunes {
inputUsername = strings . ReplaceAll ( inputUsername , string ( c ) , fmt . Sprintf ( "\\%c" , c ) )
2020-01-20 19:34:53 +00:00
}
2020-05-05 19:35:32 +00:00
2020-03-30 22:36:04 +00:00
return inputUsername
2020-01-20 19:34:53 +00:00
}
2020-03-15 07:10:25 +00:00
type ldapUserProfile struct {
2020-06-19 10:50:21 +00:00
DN string
Emails [ ] string
DisplayName string
Username string
2020-03-15 07:10:25 +00:00
}
2019-04-24 21:52:08 +00:00
2020-03-30 22:36:04 +00:00
func ( p * LDAPUserProvider ) resolveUsersFilter ( userFilter string , inputUsername string ) string {
inputUsername = p . ldapEscape ( inputUsername )
2020-12-03 05:23:52 +00:00
// The {input} placeholder is replaced by the users username input.
2020-03-30 22:36:04 +00:00
userFilter = strings . ReplaceAll ( userFilter , "{input}" , inputUsername )
return userFilter
}
func ( p * LDAPUserProvider ) getUserProfile ( conn LDAPConnection , inputUsername string ) ( * ldapUserProfile , error ) {
userFilter := p . resolveUsersFilter ( p . configuration . UsersFilter , inputUsername )
logging . Logger ( ) . Tracef ( "Computed user filter is %s" , userFilter )
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
2020-03-15 07:10:25 +00:00
attributes := [ ] string { "dn" ,
2020-06-19 10:50:21 +00:00
p . configuration . DisplayNameAttribute ,
2020-03-15 07:10:25 +00:00
p . configuration . MailAttribute ,
p . configuration . UsernameAttribute }
2020-04-20 21:03:38 +00:00
// Search for the given username.
2019-04-24 21:52:08 +00:00
searchRequest := ldap . NewSearchRequest (
baseDN , ldap . ScopeWholeSubtree , ldap . NeverDerefAliases ,
2020-03-15 07:10:25 +00:00
1 , 0 , false , userFilter , attributes , nil ,
2019-04-24 21:52:08 +00:00
)
2020-03-15 07:10:25 +00:00
sr , err := conn . Search ( searchRequest )
2019-04-24 21:52:08 +00:00
if err != nil {
2020-03-30 22:36:04 +00:00
return nil , fmt . Errorf ( "Cannot find user DN of user %s. Cause: %s" , inputUsername , err )
2019-04-24 21:52:08 +00:00
}
2020-03-15 07:10:25 +00:00
if len ( sr . Entries ) == 0 {
2020-05-04 19:39:25 +00:00
return nil , ErrUserNotFound
2019-04-24 21:52:08 +00:00
}
2020-03-15 07:10:25 +00:00
if len ( sr . Entries ) > 1 {
2020-03-30 22:36:04 +00:00
return nil , fmt . Errorf ( "Multiple users %s found" , inputUsername )
2019-04-24 21:52:08 +00:00
}
2020-03-15 07:10:25 +00:00
userProfile := ldapUserProfile {
DN : sr . Entries [ 0 ] . DN ,
2019-04-24 21:52:08 +00:00
}
2020-05-05 19:35:32 +00:00
2020-03-15 07:10:25 +00:00
for _ , attr := range sr . Entries [ 0 ] . Attributes {
2020-06-19 10:50:21 +00:00
if attr . Name == p . configuration . DisplayNameAttribute {
userProfile . DisplayName = attr . Values [ 0 ]
}
2020-03-15 07:10:25 +00:00
if attr . Name == p . configuration . MailAttribute {
userProfile . Emails = attr . Values
2020-04-15 12:26:23 +00:00
}
2020-05-05 19:35:32 +00:00
2020-04-15 12:26:23 +00:00
if attr . Name == p . configuration . UsernameAttribute {
2020-03-15 07:10:25 +00:00
if len ( attr . Values ) != 1 {
2020-03-30 22:36:04 +00:00
return nil , fmt . Errorf ( "User %s cannot have multiple value for attribute %s" ,
inputUsername , p . configuration . UsernameAttribute )
2020-03-15 07:10:25 +00:00
}
2020-05-05 19:35:32 +00:00
2020-03-15 07:10:25 +00:00
userProfile . Username = attr . Values [ 0 ]
}
2019-04-24 21:52:08 +00:00
}
2020-03-15 07:10:25 +00:00
if userProfile . DN == "" {
2020-03-30 22:36:04 +00:00
return nil , fmt . Errorf ( "No DN has been found for user %s" , inputUsername )
2019-04-24 21:52:08 +00:00
}
2020-03-15 07:10:25 +00:00
return & userProfile , nil
2019-04-24 21:52:08 +00:00
}
2020-04-09 01:05:17 +00:00
func ( p * LDAPUserProvider ) resolveGroupsFilter ( inputUsername string , profile * ldapUserProfile ) ( string , error ) { //nolint:unparam
2020-03-30 22:36:04 +00:00
inputUsername = p . ldapEscape ( inputUsername )
2020-12-03 05:23:52 +00:00
// The {input} placeholder is replaced by the users username input.
groupFilter := strings . ReplaceAll ( p . configuration . GroupsFilter , "{input}" , inputUsername )
2020-05-05 19:35:32 +00:00
2020-03-30 22:36:04 +00:00
if profile != nil {
groupFilter = strings . ReplaceAll ( groupFilter , "{username}" , ldap . EscapeFilter ( profile . Username ) )
groupFilter = strings . ReplaceAll ( groupFilter , "{dn}" , ldap . EscapeFilter ( profile . DN ) )
2019-04-24 21:52:08 +00:00
}
2020-03-30 22:36:04 +00:00
return groupFilter , nil
2019-04-24 21:52:08 +00:00
}
// GetDetails retrieve the groups a user belongs to.
2020-03-30 22:36:04 +00:00
func ( p * LDAPUserProvider ) GetDetails ( inputUsername string ) ( * UserDetails , error ) {
2019-04-24 21:52:08 +00:00
conn , err := p . connect ( p . configuration . User , p . configuration . Password )
if err != nil {
return nil , err
}
defer conn . Close ( )
2020-03-30 22:36:04 +00:00
profile , err := p . getUserProfile ( conn , inputUsername )
if err != nil {
return nil , err
}
groupsFilter , err := p . resolveGroupsFilter ( inputUsername , profile )
2019-04-24 21:52:08 +00:00
if err != nil {
2020-03-30 22:36:04 +00:00
return nil , fmt . Errorf ( "Unable to create group filter for user %s. Cause: %s" , inputUsername , err )
2019-04-24 21:52:08 +00:00
}
2020-05-05 19:35:32 +00:00
2020-03-30 22:36:04 +00:00
logging . Logger ( ) . Tracef ( "Computed groups filter is %s" , groupsFilter )
2019-04-24 21:52:08 +00:00
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
2020-04-20 21:03:38 +00:00
// Search for the given username.
2019-04-24 21:52:08 +00:00
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 {
2020-03-30 22:36:04 +00:00
return nil , fmt . Errorf ( "Unable to retrieve groups of user %s. Cause: %s" , inputUsername , err )
2019-04-24 21:52:08 +00:00
}
groups := make ( [ ] string , 0 )
2020-05-05 19:35:32 +00:00
2019-04-24 21:52:08 +00:00
for _ , res := range sr . Entries {
2020-02-27 22:21:07 +00:00
if len ( res . Attributes ) == 0 {
2020-03-30 22:36:04 +00:00
logging . Logger ( ) . Warningf ( "No groups retrieved from LDAP for user %s" , inputUsername )
2020-02-27 22:21:07 +00:00
break
}
2020-04-20 21:03:38 +00:00
// Append all values of the document. Normally there should be only one per document.
2019-04-24 21:52:08 +00:00
groups = append ( groups , res . Attributes [ 0 ] . Values ... )
}
return & UserDetails {
2020-06-19 10:50:21 +00:00
Username : profile . Username ,
DisplayName : profile . DisplayName ,
Emails : profile . Emails ,
Groups : groups ,
2019-04-24 21:52:08 +00:00
} , nil
}
// UpdatePassword update the password of the given user.
2020-03-30 22:36:04 +00:00
func ( p * LDAPUserProvider ) UpdatePassword ( inputUsername string , newPassword string ) error {
2019-04-24 21:52:08 +00:00
client , err := p . connect ( p . configuration . User , p . configuration . Password )
if err != nil {
return fmt . Errorf ( "Unable to update password. Cause: %s" , err )
}
2020-03-30 22:36:04 +00:00
profile , err := p . getUserProfile ( client , inputUsername )
2019-04-24 21:52:08 +00:00
if err != nil {
return fmt . Errorf ( "Unable to update password. Cause: %s" , err )
}
2020-03-15 07:10:25 +00:00
modifyRequest := ldap . NewModifyRequest ( profile . DN , nil )
2019-04-24 21:52:08 +00:00
2020-11-27 09:59:22 +00:00
switch p . configuration . Implementation {
case schema . LDAPImplementationActiveDirectory :
utf16 := unicode . UTF16 ( unicode . LittleEndian , unicode . IgnoreBOM )
// The password needs to be enclosed in quotes
// https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/6e803168-f140-4d23-b2d3-c3a8ab5917d2
pwdEncoded , _ := utf16 . NewEncoder ( ) . String ( fmt . Sprintf ( "\"%s\"" , newPassword ) )
modifyRequest . Replace ( "unicodePwd" , [ ] string { pwdEncoded } )
default :
modifyRequest . Replace ( "userPassword" , [ ] string { newPassword } )
}
2019-04-24 21:52:08 +00:00
err = client . Modify ( modifyRequest )
if err != nil {
return fmt . Errorf ( "Unable to update password. Cause: %s" , err )
}
return nil
}