wiltonsr/ldapAuth

BindDN and BindPassword are not used for AllowedGroups checking

sasjafor opened this issue · 8 comments

When using the options BindDN and BindPassword in combination with AllowedGroups then the AllowedGroups are checked using the user credentials instead of the bind credentials.

Is this intentional behaviour?

For me this creates an issue since the individual users cannot access group membership information. Only the bind user has access to those parts of the directory.

If it's unintentional, consider this a bug report. If it's intentional, then a feature request for a toggle to choose the behaviour.

Hi, @sasjafor

Could you provide your confs and debug logs?

When using the options BindDN and BindPassword in combination with AllowedGroups then the AllowedGroups are checked using the user credentials instead of the bind credentials.

Is this intentional behaviour?

Please check Operations Mode docs. All subsequent LDAP operations will be performed based on the Operation Mode defined in confs. You can also check this mode by checking ldapAuth's logs.

You can also check this example. I think you need to set searchFilter option to perform what you need.

Hello @wiltonsr,

I checked the source code and I saw that the method ServeHTTP first calls LdapCheckUser and then LdapCheckUserAuthorized.
LdapCheckUser performs a bind on the LDAP connection with the credentials of the user trying to sign in.
Then there is no further bind until LdapCheckUserAuthorized is called, where the group membership is checked, while still bound using the user credentials instead of the BindDN and BindPassword. This is not mentioned in the docs as far as I could see.

The example you provided does not use AllowedGroups and as such does not apply to my use case.

Here is my config:

http:
  middlewares:
    my-ldapAuth:
      plugin:
        ldapAuth:
          Enabled: true
          LogLevel: "DEBUG"
          Url: "ldap://mydomain.example"
          Port: 389
          BaseDN: "dc=company,dc=local"
          Attribute: "uid"
          BindDN: "cn=read,dc=company,dc=local"
          BindPassword: "<censored>"
          AllowedGroups:
            - "cn=root,ou=groups,dc=company,dc=local"
          SearchFilter: (\{\{.Attribute\}\}=\{\{.Username\}\})

And the debug log:

traefik    | time="2024-03-20T10:08:47Z" level=info msg="Configuration loaded from flags."
traefik    | INFO: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: Starting my-ldapAuth@file Middleware...
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: Enabled => 'true'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: LogLevel => 'DEBUG'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: URL => 'ldap://mydomain.example'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: Port => '389'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: CacheTimeout => '300'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: CacheCookieName => 'ldapAuth_session_token'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: CacheCookiePath => ''
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: CacheCookieSecure => 'false'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: CacheKey => 'super-secret-key'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: StartTLS => 'false'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: InsecureSkipVerify => 'false'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: MinVersionTLS => 'tls.VersionTLS12'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: MaxVersionTLS => 'tls.VersionTLS13'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: CertificateAuthority => ''
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: Attribute => 'uid'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: SearchFilter => '(\{\{.Attribute\}\}=\{\{.Username\}\})'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: BaseDN => 'dc=company,dc=local'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: BindDN => 'cn=read,dc=company,dc=local'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: BindPassword => '<censored>'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: ForwardUsername => 'true'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: ForwardUsernameHeader => 'Username'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: ForwardAuthorization => 'false'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: ForwardExtraLdapHeaders => 'false'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: WWWAuthenticateHeader => 'true'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: WWWAuthenticateHeaderRealm => ''
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: EnableNestedGroupFilter => 'false'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: AllowedGroups => '[cn=root,ou=groups,dc=company,dc=local]'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: AllowedUsers => '[]'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:08:47 restricted.go:51: Username => ''
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: Session details: &{ map[] 0xc00166e140 true {0xc002b8b040 {0xc002df56c0 0xc0020c12a8 406}} ldapAuth_session_token}
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:52: No session found! Trying to authenticate in LDAP
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: Connect Address: 'ldap://mydomain.example:389'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: Running in Search Mode
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: Performing User BindDN Search
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: Search Filter: '(uid=testuser)'
traefik    | INFO: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: Authenticating User: cn=Test User,ou=users,dc=company,dc=local
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: Group Filter: '(|(member=cn=Test User,ou=users,dc=company,dc=local)(uniqueMember=cn=Test User,ou=users,dc=company,dc=local)(memberUid=testuser))'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: Searching Group: 'cn=root,ou=groups,dc=company,dc=local' with User: 'cn=Test User,ou=users,dc=company,dc=local'
traefik    | INFO: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: LDAP Result Code 32 "No Such Object": 
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: User: 'testuser' not found in Group: 'cn=root,ou=groups,dc=company,dc=local'
traefik    | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:52: [LDAP Result Code 32 "No Such Object": 
traefik    | User 'testuser' does not match any allowed users nor allowed groups.]
traefik    | ERROR: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: LDAP Result Code 32 "No Such Object": 
traefik    | User 'testuser' does not match any allowed users nor allowed groups.

LdapCheckUser performs a bind on the LDAP connection with the credentials of the user trying to sign in.

This only happens if your searchFilter is empty.

ldapAuth/ldapauth.go

Lines 251 to 264 in 5901443

// LdapCheckUser check if user and password are correct.
func LdapCheckUser(conn *ldap.Conn, config *Config, username, password string) (bool, *ldap.Entry, error) {
if config.SearchFilter == "" {
LoggerDEBUG.Printf("Running in Bind Mode")
userDN := fmt.Sprintf("%s=%s,%s", config.Attribute, username, config.BaseDN)
userDN = strings.Trim(userDN, ",")
LoggerDEBUG.Printf("Authenticating User: %s", userDN)
err := conn.Bind(userDN, password)
return err == nil, ldap.NewEntry(userDN, nil), err
}
LoggerDEBUG.Printf("Running in Search Mode")
result, err := SearchMode(conn, config)

Otherwise, the function SearchMode will be called

ldapAuth/ldapauth.go

Lines 463 to 474 in 5901443

// SearchMode make search to LDAP and return results.
func SearchMode(conn *ldap.Conn, config *Config) (*ldap.SearchResult, error) {
if config.BindDN != "" && config.BindPassword != "" {
LoggerDEBUG.Printf("Performing User BindDN Search")
err := conn.Bind(config.BindDN, config.BindPassword)
if err != nil {
return nil, fmt.Errorf("BindDN Error: %w", err)
}
} else {
LoggerDEBUG.Printf("Performing AnonymousBind Search")
_ = conn.UnauthenticatedBind("")
}

And the bind will be made authenticated or anonymous based on bindDN and bindPassword options.

The example you provided does not use AllowedGroups and as such does not apply to my use case.

There is an example using AllowedGroups. But your confs looks OK and you are running in SearchMode

traefik | DEBUG: ldapAuth: 2024/03/20 10:09:24 restricted.go:51: Running in Search Mode

Could you provide a ldapsearch in your "cn=root,ou=groups,dc=company,dc=local" group?

An example using ldap.forumsys.com will be

ldapsearch  -x \
  -b "ou=mathematicians,dc=example,dc=com" \
  -H ldap://ldap.forumsys.com \
  -D "uid=tesla,dc=example,dc=com" \
  -w password

In this case we got

# extended LDIF
#
# LDAPv3
# base <ou=mathematicians,dc=example,dc=com> with scope subtree
# filter: (objectclass=*)
# requesting: ALL
#

# mathematicians, example.com
dn: ou=mathematicians,dc=example,dc=com
uniqueMember: uid=euclid,dc=example,dc=com
uniqueMember: uid=riemann,dc=example,dc=com
uniqueMember: uid=euler,dc=example,dc=com
uniqueMember: uid=gauss,dc=example,dc=com
uniqueMember: uid=test,dc=example,dc=com
ou: mathematicians
cn: Mathematicians
objectClass: groupOfUniqueNames
objectClass: top

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

That matches with the Group Filter with the attribute uniqueMember

ldapAuth/ldapauth.go

Lines 338 to 345 in 5901443

templ := "(|" +
"(member={{.UserDN}})" +
"(uniqueMember={{.UserDN}})" +
"(memberUid={{.Username}})" +
"{{if .EnableNestedGroupFilter}}" +
"(member:1.2.840.113556.1.4.1941:={{.UserDN}})" +
"{{end}}" +
")"

Yes, SearchMode is called, but later in LdapCheckUser a bind is performed again also using the user credentials:

err = conn.Bind(userDN, password)

I added

conn.Bind(la.config.BindDN, la.config.BindPassword)

before LdapCheckUserAuthorized is called and that fixed my problem.

ldapAuth/ldapauth.go

Lines 194 to 202 in 5de938d

if !isValidUser {
defer conn.Close()
LoggerERROR.Printf("%s", err)
LoggerERROR.Printf("Authentication failed")
RequireAuth(rw, req, la.config, err)
return
}
isAuthorized, err := LdapCheckUserAuthorized(conn, la.config, entry, username)

Yes, SearchMode is called, but later in LdapCheckUser a bind is performed again also using the user credentials:

You are right, thanks for pointing that out.

EDIT: I will work on this in this branch. I would like to ping you to test the fix as soon as possible.

Available in v0.1.8.

I tested and now it works as expected. Thanks for the quick fix!